312
1

言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1

言 語 プ ロ セ ッ サ

Page 2: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

2

2017.8.22 再整形

Page 3: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

序   文 3

序   文

計算機のプログラミングを行うにあたって,「言語プロセッサ」ないし「言語処理系」,

すなわちプログラミング言語による記述を入力とし,その記述を実行可能とするよう

な一群のソフトウェアは不可欠のものである.その中でも,高水準言語を入力としア

センブリ言語ないし機械語を生成するプログラム,すなわち「コンパイラ」はその中

心的位置を占めている.コンパイラに使用されている技術は形式言語理論を始めとす

る理論的なものからプログラムの自動生成系や各種ツールなどまで非常に多岐に渡り,

しかも翻訳時間や生成されるコードの質など実用面での要求も厳しい.本書はこのよ

うな多様な側面をもつコンパイラを中心に,言語処理系に見られる各種技術について

解説していく.

「コンパイラ」という言葉は,計算機科学を学ぶ学生にとっていくらか特別の意味

をもっているように思われる.つまり,「いつかコンパイラぐらい書けるようになりた

い…」というわけである.確かにオペレーティングシステムとなると 1人で書くには

少し大きすぎてしりごみしそうだが,コンパイラくらいなら 1人でも十分書けそうで

ある.

そして,コンパイラをつくることはある点では着実にやさしくなりつつある.コン

パイラの各部をどうつくるのがよいか,という理論的研究の進展と,それらに裏付け

られた,種々のツールの出現がその背景になっている.そのため,簡潔だが単なるお

もちゃではないコンパイラをつくることは,今では学部レベルの学生にとっても十分

こなせる課題となっている.

しかし一方で,コンパイラをつくることは着実に難しくもなりつつある.新しい計

Page 4: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4 序   文

算機アーキテクチャには,これまでハードウェアで実行時に行ってきた部分をソフト

ウェア (つまりコンパイラ)に移すことによって高速化しようとする傾向がある.その

結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

ケジューリング,並列化,その他の特殊なハードウェア機構の活用,など絶えず新し

い課題がコンパイラのうえに課せられつつある.

本書は多少なりともプログラミングの経験をもち,コンパイラに接したことのある

読者を対象とし,プログラミング言語がどのような経過を経て裸の計算機上で実行さ

れるようになるのかを一通り理解して頂くことを目指している.その際,上記のよう

なコンパイラを取り巻く状況の 2 面性を考慮し,読者の目的に応じて次のようにいく

つかの段階に分けた目標を立てることにする.

まず,本書の第 1目標としては,言語処理系の全体構造と各部の役割を理解してい

ただくことを掲げる.そのためにはまず 1章を通読し,コンパイラを含む言語処理系

の構造について学んで頂きたい.その後は各自の興味に従い,続く各章の中から興味

のある部分を適宜読んで頂ければよい.その際,必ずしもそれぞれの部分がどのよう

な原理に従って処理を進めているのかを理解する必要はなく,その機能や入出力につ

いて実例中心に理解していただければ十分である.

次に,第 2目標として,言語処理系の各部が行う処理の内容を具体的に把握し,簡

単なコンパイラを作成できるようになって頂くことを掲げる.そのためには第 1目標

のレベルに加えて,いくつかのツールを使いこなせる程度に例題を読みこなし,また

ソースコードと目的コードの対応関係を具体的に把握できるようになる必要がある.こ

の段階では一応ツールを使用した例題が十分理解できればよいが,できれば各ツール

の背後にある動作原理までを理解しておくことが望ましい.一方,最適化やコード生

成器生成系などについては当面置いておいてもかまわない.

さらに進んで,第 3目標としては,より高い品質のコンパイラを作成するために必

要な知識をもって頂くことを掲げる.そのためには各ツールの動作原理や最適化,コー

ド生成における機械依存性などのテーマについても理解して頂く必要がある.

しかし実際にはこれだけでは十分ではない.前述のように,コンパイラの技術は計

算機そのものの発展と並行して日進月歩であり,絶えず新しいアイデアや技法が提案

され,より進んだコンパイラがつくられていく.その中にはこれまでのコンパイラで

は全く省みられなかったような事柄を扱うものも多い.その種類は多岐にわたり,到

底本書でカバーできるようなものではない.結局,これらの「本当に新しい」事柄を

Page 5: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

序   文 5

学ぶにはそれについて記された論文を読む以外の方法はない.ただし,そのような新

しいものに接して「わかる」ための土台を本書が提供できれば幸いである.

これら各段階の目標を通じて,コンパイラについて本当に理解するには単に本を読

むだけではなく,実際に手を動かしてその感触を体験することが重要である.そのた

め,本書では単なる原理の説明だけでなく,できるだけ実際に試してみられるような

実例を多く含めることを目指した.類書ではアルゴリズムや原理などについて説明す

るにとどめてある部分についても,本書ではできるだけ動かしてみられるプログラム

を掲載するようにした.筆者はプログラマとしては特にセンスが良いわけでも何でも

ないので,掲載したプログラムを省みては赤面の思いにとらわれるのだけれど,疑似

コードなどで原理だけを説明したものと実際に動くプログラムとでは説得力が違うと

思うので,恥を忍んであえてそうさせて頂いた.スタイルが好ましくない等のご批判

もあると思うけれど,そういうわけでご容赦頂きたい.

プログラムに使用する言語としてはLisp(Common Lisp)とCを採用した.元来,コ

ンパイラの処理には記号計算的な要素が多く含まれていおり,そのような部分は当然,

記号処理を目指して設計された Lispのような言語で書く方がコンパクトでわかりやす

い.プログラム自体はそんなに難しい機能を使ったりするわけではないので,Lisp言

語の経験のない人でも手近の参考書などを少し参照していただければ十分わかるもの

と思われる.本書末尾にも付録として簡単なLisp言語の紹介を含めた.Cについては,

lexや yaccなどのツールと組み合わせるのになじみがよく,これらを利用して簡単な

処理系を組んで動かしてみるのに好適だからである.

ただし,本書でプログラムを掲載する目的は「完全なコンパイラの実例を与える」こ

とではなく「コンパイラ各部の働きを抽出して動かして見せる」ところにある.だか

ら本書に出てくるプログラムを全部打ち込んだら動くコンパイラができるわけではな

い,という点についてはご理解頂きたい.

最後になったが,本書の内容は言語処理系および計算機システム全般について筆者

に様々なことを教えて下さった多くの先生がた,とくに井上謙三先生 (東京理科大学),

木村泉先生 (東京工業大学),故島内剛一先生 (立教大学)に多くを負っている.また,

筆者の講義を聴講してくれた筑波大学大学院経営システム科学専攻,東京工業大学理

学部情報科学科,電気通信大学情報工学科の学生諸君からも多くの貴重な反応を頂い

た.ここに感謝の意を著したい.

そして,つたない原稿に目を通し有益なコメントを下さった本シリーズ監修者の土

Page 6: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6 序   文

居範久先生 (慶應義塾大学)と稲垣康善先生 (名古屋大学),ならびに原稿の完成を辛抱

強く待って頂いた丸善株式会社出版事業部の池田和博氏にも掛けたご迷惑をお詫びす

るとともに,心から感謝したい.

1993年 8月

久野 靖

Page 7: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

7

目 次

第 1章 言語処理系の位置づけと概観 13

1.1 プログラミング言語と言語処理系 . . . . . . . . . . . . . . . . . . . . . 13

1.2 言語の構文と意味 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

1.3 プログラミング言語処理系 . . . . . . . . . . . . . . . . . . . . . . . . . 17

1.4 コンパイラの諸成分 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

1.5 コンパイラの周辺 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

1.6 インタプリタの諸成分 . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

1.7 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

第 2章 形 式 言 語 29

2.1 形式言語の諸定義 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

2.2 文法と言語の認識/解析 . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

2.3 文脈自由文法とその記法 . . . . . . . . . . . . . . . . . . . . . . . . . . 33

2.4 正規文法と正規表現 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

2.5 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

第 3章 字 句 解 析 37

3.1 字句定義の正規表現と有限オートマトン . . . . . . . . . . . . . . . . . 37

3.2 正規表現から非決定性有限オートマトンへの変換 . . . . . . . . . . . . 39

3.3 非決定性有限オートマトンから決定性有限オートマトンへの変換 . . . . 44

3.4 字句解析器生成系 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

3.5 字句解析の実際 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

3.6 ハンドコーディングによる字句解析 . . . . . . . . . . . . . . . . . . . . 53

3.7 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

Page 8: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8 序   文

第 4章 構 文 解 析 57

4.1 構文解析と構文木 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

4.2 下 向 き 解 析 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

4.2.1 下向き解析と First/Follow . . . . . . . . . . . . . . . . . . . . 58

4.2.2 LL(1)解析器と LL(1)文法 . . . . . . . . . . . . . . . . . . . . . 63

4.2.3 再帰下降解析器 . . . . . . . . . . . . . . . . . . . . . . . . . . . 67

4.3 上 向 き 解 析 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

4.3.1 シフト-還元解析器 . . . . . . . . . . . . . . . . . . . . . . . . . 70

4.3.2 LRオートマトンと項 . . . . . . . . . . . . . . . . . . . . . . . . 72

4.3.3 LR(0)オートマトンの作成 . . . . . . . . . . . . . . . . . . . . . 74

4.3.4 SLR(1)解析器 . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

4.3.5 正準 LR(1)解析器 . . . . . . . . . . . . . . . . . . . . . . . . . 83

4.3.6 LALR(1)解析器 . . . . . . . . . . . . . . . . . . . . . . . . . . 87

4.3.7 再帰上昇解析器 . . . . . . . . . . . . . . . . . . . . . . . . . . . 90

4.4 実用のための構文解析 . . . . . . . . . . . . . . . . . . . . . . . . . . . 93

4.4.1 曖 昧 な 文 法 . . . . . . . . . . . . . . . . . . . . . . . . . . . 93

4.4.2 誤りからの回復 . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

4.4.3 解析表の圧縮 . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

4.5 構文解析器生成系 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

4.6 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101

第 5章 意 味 解 析 105

5.1 意味解析の役割と位置づけ . . . . . . . . . . . . . . . . . . . . . . . . . 105

5.2 属 性 文 法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107

5.3 構文指示翻訳 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

5.4 構文指示翻訳の実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

5.4.1 下向き解析器における構文指示翻訳 . . . . . . . . . . . . . . . . 111

5.4.2 上向き解析器における構文指示翻訳 . . . . . . . . . . . . . . . . 114

5.5 構文木の生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116

5.6 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121

Page 9: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

序   文 9

第 6章 記  号  表 123

6.1 記号表の位置づけ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

6.2 表の概念と実現技法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

6.2.1 表 の 概 念 . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

6.2.2 1次元配列による表 . . . . . . . . . . . . . . . . . . . . . . . . . 124

6.2.3 2分木による表 . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

6.2.4 ハ ッ シ ュ 表 . . . . . . . . . . . . . . . . . . . . . . . . . . . 126

6.3 記号表の特質 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

6.3.1 文 字 列 領 域 . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

6.3.2 ブロック型スコープの処理 . . . . . . . . . . . . . . . . . . . . . 130

6.3.3 スコープ規則の拡張 . . . . . . . . . . . . . . . . . . . . . . . . 133

6.4 記号表に関連する言語処理系の問題 . . . . . . . . . . . . . . . . . . . . 135

6.4.1 前方参照の問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

6.4.2 分 割 翻 訳 . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

6.5 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138

第 7章 型  検  査 141

7.1 型の役割りと位置づけ . . . . . . . . . . . . . . . . . . . . . . . . . . . 141

7.2 型 の 同 一 性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

7.3 宣 言 の 処 理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

7.3.1 型指定の処理 . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

7.3.2 宣言部の処理 . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

7.4 式における型の同定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

7.5 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

第 8章 実 行 時 環 境 153

8.1 実行時環境と名前の束縛 . . . . . . . . . . . . . . . . . . . . . . . . . . 153

8.2 記憶領域の種別と割当て . . . . . . . . . . . . . . . . . . . . . . . . . . 154

8.3 スタックとスタックフレーム . . . . . . . . . . . . . . . . . . . . . . . . 156

8.3.1 スタックの用途 . . . . . . . . . . . . . . . . . . . . . . . . . . . 156

8.3.2 スタックフレームとフレームポインタ . . . . . . . . . . . . . . 156

Page 10: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10 序   文

8.3.3 引数と返値の受け渡し . . . . . . . . . . . . . . . . . . . . . . . 159

8.3.4 レジスタの退避回復 . . . . . . . . . . . . . . . . . . . . . . . . 164

8.4 環境の切換え . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165

8.4.1 静 的 チェイ ン . . . . . . . . . . . . . . . . . . . . . . . . . . . 165

8.4.2 ディス プ レ イ . . . . . . . . . . . . . . . . . . . . . . . . . . . 167

8.4.3 手 続 き 引 数 . . . . . . . . . . . . . . . . . . . . . . . . . . . 168

8.5 ヒープとその管理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169

8.5.1 ヒープの位置付けと機能 . . . . . . . . . . . . . . . . . . . . . . 169

8.5.2 手動による領域回復 . . . . . . . . . . . . . . . . . . . . . . . . 170

8.5.3 ご み 集 め . . . . . . . . . . . . . . . . . . . . . . . . . . . 172

8.6 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177

第 9章 中間コード生成 179

9.1 目的コードと中間コード . . . . . . . . . . . . . . . . . . . . . . . . . . 179

9.2 様々な中間コード . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180

9.2.1 木構造の中間コード . . . . . . . . . . . . . . . . . . . . . . . . 180

9.2.2 非循環有向グラフ . . . . . . . . . . . . . . . . . . . . . . . . . . 181

9.2.3 後 置 コ ー ド . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

9.2.4 4つ組と 3つ組 . . . . . . . . . . . . . . . . . . . . . . . . . . . 184

9.3 中間コード生成の実例 . . . . . . . . . . . . . . . . . . . . . . . . . . . 185

9.3.1 枠 組 の 説 明 . . . . . . . . . . . . . . . . . . . . . . . . . . . 185

9.3.2 データ構造の参照と代入 . . . . . . . . . . . . . . . . . . . . . . 187

9.3.3 式 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192

9.3.4 制 御 構 造 . . . . . . . . . . . . . . . . . . . . . . . . . . . 197

9.3.5 手  続  き . . . . . . . . . . . . . . . . . . . . . . . . . . . 206

9.3.6 まとまった実行例と共通式の削除 . . . . . . . . . . . . . . . . . 209

9.4 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212

第 10章 最  適  化 215

10.1 最適化の原理と分類 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215

10.2 最適化のためのコード解析 . . . . . . . . . . . . . . . . . . . . . . . . . 216

Page 11: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

序   文 11

10.2.1 基本ブロックとフローグラフ . . . . . . . . . . . . . . . . . . . 216

10.2.2 制御フロー解析 . . . . . . . . . . . . . . . . . . . . . . . . . . . 220

10.2.3 データフロー解析 . . . . . . . . . . . . . . . . . . . . . . . . . . 224

10.2.4 記号実行と範囲解析 . . . . . . . . . . . . . . . . . . . . . . . . 231

10.3 一般の各種最適化 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231

10.3.1 最適化の各種手法の分類 . . . . . . . . . . . . . . . . . . . . . . 231

10.3.2 一般の最適化の例 . . . . . . . . . . . . . . . . . . . . . . . . . . 234

10.4 ループ最適化 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242

10.5 手続き間最適化 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250

10.6 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254

第 11章 目的コード生成 255

11.1 組単位の展開によるコード生成 . . . . . . . . . . . . . . . . . . . . . . 255

11.2 解釈実行型コード生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . 260

11.3 レジスタ割当てとレジスタ割付け . . . . . . . . . . . . . . . . . . . . . 262

11.4 目的コードの改良とのぞき穴最適化 . . . . . . . . . . . . . . . . . . . . 268

11.5 コード生成器生成系 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270

11.5.1 記述主導型コード生成 . . . . . . . . . . . . . . . . . . . . . . . 270

11.5.2 木のパターンマッチによるコード生成 . . . . . . . . . . . . . . 270

11.5.3 Graham-Granvilleコード生成器 . . . . . . . . . . . . . . . . . . 276

11.5.4 Davidson-Fraserコード生成器 . . . . . . . . . . . . . . . . . . . 277

11.6 様々な目的機械の特性への対処 . . . . . . . . . . . . . . . . . . . . . . 280

11.6.1 命令スケジューリング . . . . . . . . . . . . . . . . . . . . . . . 280

11.6.2 RISCアーキテクチャ . . . . . . . . . . . . . . . . . . . . . . . 281

11.6.3 ベクトルプロセッサとマルチプロセッサ . . . . . . . . . . . . . 284

11.7 練 習 問 題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286

付 録A Lispプログラムの読み方 287

付 録B 参 考 文 献 301

B.1 コンパイラの教科書 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301

B.2 ほんもののコンパイラ . . . . . . . . . . . . . . . . . . . . . . . . . . . 302

Page 12: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

12 序   文

B.3 マクロプロセッサ・プリプロセッサ・ツール . . . . . . . . . . . . . . . 303

B.4 C言語と Lisp言語 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304

B.5 その他の単行本 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304

B.6 各種論文・解説 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305

Page 13: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

13

第1章 言語処理系の位置づけと概観

本章はまず序論として,本書で対象としている言語処理系が扱うような言語とはどん

なものか,それはどのようにして定義されるのが普通か,それを処理する言語処理系

は一般にどのような構造をしているか,などについて取り上げ,引き続く各章の全体

的なつながりに関する概観を与えることを目標とする.

1.1 プログラミング言語と言語処理系

図 1.1は,計算機による情報処理のモデルをごく単純化したものである.これによれ

ば,計算機とは外界から情報を受け取り (入力),内部で処理し,外界にその結果を引き

渡す (出力)ものだと考えられる.ここで計算機に情報を入力する一般的な方法は (少

なくとも現状では)「文字や記号の列として与える」ことである.したがって計算機は

入力された文字の列から必要な情報を抽出することになる.ここで重要なのは,情報

を渡す側と受け取る側の間で,その文字の列の構造や意味に関する規則についての合

意が必要だという点である.人間どうしの場合でいえば,「英語で伝達する」とか「こ

れこれの暗号で伝達する」といったことがこれに相当する.

では人間から計算機に情報を渡すに際してどのような「規則」を合意したらよいだ

ろう.1つの方法は自然言語を使うことであるが,自然言語を計算機で解読し,それを

発した人間の意図を正確に捉えることは現在の技術ではまだ困難である.もう 1つの

入力情報 計算機システム 出力情報

図 1.1: 計算機システムと入出力の概念

Page 14: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

14 第 1章 言語処理系の位置づけと概観

方法は,計算機で効率よく処理できる,自然言語よりはずっと簡単な人工的言語 (=計

算機言語)を設定し,これによって計算機に情報を与えるという方法であり,一般には

こちらが用いられている.以下本書では計算機言語を次のように定義する.

定義 計算機言語とは,主として人間と計算機の間の効率よい情報交換のために人工的

に構成された言語である.

計算機言語とよばれるものの中には,文書整形言語,データベース言語,コマンド言

語,データ表現言語など様々な目的に合せて設計されたものが含まれる.これに対し,

プログラミング言語はこのような特定目的に依存しない計算機言語である.

定義 プログラミング言語とは,計算機内部で実行されるべき動作を汎用的に記述する

ことを目的とした計算機言語である.

どの種類にせよ,計算機言語を実際に役立てるには言語処理系が必要である.

定義 言語処理系とは,計算機言語が記述するものを実際に計算機により実行させるた

めに必要なソフトウェア群の総称である.

以下本書ではプログラミング言語とその処理系を取り上げるが,「入力言語を認識し,

その意図するところを解析する」という点は全ての計算機言語に共通しており,プロ

グラミング言語処理系の手法が広く適用できる.

もともと,計算機のハードウェアは,主記憶上にあるビット列 (命令列) を取り出し,

そのパターンに応じて動作を実行していく機能をもつ.したがって,このビット列も

上に述べた意味でのプログラミング言語である.これを機械語とよぶ.しかし,機械

語は 0 と 1の羅列であり,人間にとっては読みづらく扱いづらい.そこで,命令や番

地に適宜名前をつけ,0と 1の代りにこれらの名前を用いてプログラムを記述するよう

になった.この言語をアセンブリ言語,アセンブリ言語の処理系をアセンブラとよぶ.

アセンブリ言語と機械語を合せて低水準言語とよぶ.低水準言語のプログラムは特

定の計算機ハードウェアに依存しており,ある機種用の機械語やアセンブリ言語のプ

ログラムは他の機種では動かすことができない.その主な理由は,使用できる命令の

種類,およびプログラミングにおいて使用される概念 (レジスタ,番地づけ,番地方式

など)が機種によって異なるためである.これらの一群の仕様を命令セットアーキテク

チャ(Instruction Set Architecture,ISA)とよぶ.

Page 15: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1.2. 言語の構文と意味 15

定義 特定の命令セットアーキテクチャに依存したプログラミング言語を低水準言語と

よぶ.

低水準言語によるプログラムは他の機種に移すことができないうえに,記述にも手数

がかかる.そこで,「番地」「命令」「レジスタ」など機械依存の概念の代りに「変数」

「演算式」「手続き」など抽象度の高い概念に基づいて構成され,特定の機種に依存し

ない言語を用いてプログラミングを行うことが現在では一般的になっている.このよ

うな言語を高水準言語とよぶ.

定義 特定の命令セットアーキテクチャに依存しないプログラミング言語を高水準言

語とよぶ.

以下本書では高水準言語の処理系を扱う.例えば 1から 100までの整数の合計を計

算する C言語のコード (プログラムの断片)を見てみる.

x = i = 0;

while(++i <= 100)

x += i;

ここに現れる x,iなどの変数は,計算の過程で現れる値を保持する抽象化された記憶

場所を表している.変数はこのプログラムが実行されるときにはどこかのレジスタや

主記憶上の場所に写影された形で存在する.同様に,++(1増やす),+(加算),<=(大小

比較)などの演算子も具体的な命令のどれかに写影されて実行されることになる.した

がって,高水準言語の本質は実行されるべき計算過程を抽象化された形で記述するこ

とを許す点にあり,高水準言語の処理系は抽象化された記述から実際の計算過程への

写影を実現するものであるといえる.

1.2 言語の構文と意味

我々が外国語を学ぶ場合,文法 (どのような種類の単語をどのような順序ないし規則で

配列することが許されるか)と読解 (具体的な文章に接して,その内容を読み解く)を

分けて行うように,プログラミング言語においても構文と意味を分けて扱う.

Page 16: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

16 第 1章 言語処理系の位置づけと概観

構文=文字をどう並べるかの規則

文字の並び

意図=起こって欲しい事柄

意味=構文と動作の対応規則

実際に起こる  動作

システム

人間

図 1.2: 計算機言語における構文と意味の関係

定義 プログラミング言語の構文とは,そのプログラミング言語においてはどのような

種類の文字 (ないし文字の列である語)をどのような順序で配置することが許さ

れるかを定めた規則である.

定義 プログラミング言語の意味とは,構文規則にかなったプログラムがどのような動

作を行うべきであるかを定めた規則である.

なぜ構文と意味が必要なのだろうか.プログラミング言語を使う目的は計算機に自分の

望む動作を実行させることにある.そこでまず字の並び方の規則 (=構文)を定め,図

1.2に示すようにそれぞれの構文に対応して,その構文が現れた場合に計算機内部で起

きるべき動作 (= 意味)を決めておく.これを整理すると,計算機と人間の双方で構文

と意味の両規則について合意があって始めて,プログラミング言語によって人間が計

算機に望む動作を行わせられるのである.

では,具体的にはどのようにして構文や意味を規定したらよいだろう.構文につい

ては,形式言語 (formal language)という確立した枠組があり,そこで定義される正規

言語や文脈自由言語の枠組を使用すれば一般によく出て来るようなプログラミング言

語の構文は問題なく定義できる.形式言語については続く 2章で扱う.

一方,意味についても定式化のための理論的枠組を与えようとする試みは多数存在

する.しかし,現状ではどの枠組を採用しても,それによってある言語の意味全体を

記述することはかなり大変であり,また記述したものが必ずしも理解しやすいとは言

えない.そのため,意味については自然言語による記述が最も多く一般に用いられて

いる.本書でも基本的にこの立場をとる.

Page 17: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1.3. プログラミング言語処理系 17

1.3 プログラミング言語処理系

ひらたく言えば,プログラミング言語処理系の役割は,言語によって記述された動作

を実際に計算機内で実行させることであった.ところで,計算機のハードウェアは機

械語で記述された動作を実行する機能を実現する装置そのものである.そのため,プ

ログラミング言語処理系のあり方としては次の 2通りが存在する.

a. プログラミング言語により記述された動作を認識するつど,その動作をその時点

で実行する — インタプリタ (interpreter,解釈実行系).

b. プログラミング言語による記述を解読し,それが意図する動作を行う機械語 (な

いしアセンブリ言語)記述を生成する — コンパイラ (compiler,翻訳系).

これらの概念的な区別を図 1.3に示す.機械語もある種の言語であるから,その考え

方を押し進めると上記の 2つは次のものの特殊な場合であると考えることができる.

a’. 実行系 — 言語記述を入力とし,そこに記された動作を実行する.

b’. 変換系 — 言語記述を入力とし,そこに記された動作の別の言語による記述を出

力する.

ここで,変換系,実行系ともに入力される言語記述を一般にソースコードとよぶ.ま

た,変換系において出力されるものを一般に目的コードとよぶ.

通常インタプリタは機械語より高レベルの言語をソースコードとする実行系,コン

パイラは高水準言語をソースコードとし,比較的機械語に近いレベルの言語を目的コー

ドとする変換系を指す.また処理系によっては,入力言語を比較的低レベルの中間形

式に変換したのち,その中間形式を解釈実行する,というものもある.これはコンパ

イラ・インタプリタとよばれる.一般に,コンパイラは目的コードの実行速度が速く,

一方インタプリタは実行開始までの時間が短く,また実行時においても入力言語の情

報を合せて保持しているので情報収集の機能を充実させやすい.

1.4 コンパイラの諸成分

図 1.4に最小限の成分から成るコンパイラの典型的な構成を示す.その各成分の役割

は次の通り.

Page 18: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

18 第 1章 言語処理系の位置づけと概観

ソースコード

解釈実行系

翻訳系

直接動作を 実行

起動されると動作を実行

  実行可能(機械語)コード

図 1.3: コンパイラ (翻訳系)とインタプリタ (解釈実行系)の概念

ソースコード

字句解析

構文解析

意味解析

コード生 成

目的コード

記号表

図 1.4: 単純な構造をもつコンパイラの構成例

字句解析: 入力言語の文字の並びを順に走査して,「名前」「数値定数」「予約語」「演算

子」など,以降の処理において意味をもつかたまり (つづり,トークンなどとよ

ぶ)に切り分ける.また,注釈や空白部分など以降の処理に必要のない部分は切

り捨てる.例えば先の「1から 100までの整数の合計」のコードを字句解析に通

すと (概念的には)図 1.5のようなつづりの列が得られる.

構文解析: 字句解析から渡されてきたつづりの並びが構文規則に適合していることを

確認し,どの部分がどの規則に対応しているかの情報を抽出する.構文解析の出

力は例えば図 1.6のようなデータ構造で表される.

意味解析: 入力が意味規則に適合していることを確認し,さらに後段で利用する情報

を記録しておく.意味解析は特定の出力形式をもつというよりは,例えば図 1.7

のように構文解析の出力に必要な情報を付加する形をとることが多い.

コード生成: 目的コードを生成する.目的コードはアセンブリコードのこともあれば,

Page 19: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1.4. コンパイラの諸成分 19

<keyword, "int"><identifier, id# = 1><delimiter, "=">

<identifier, id# = 2>

<intliteral, value = 0><delimiter, ";">

<delimitar, "=">

<keyword, "while">

<delimiter, "("><delimiter, "++"><identifier, id# = 2><delimiter, "<="><intliteral, value = 100><delimiter, ")"><identifier, id# = 1><delimitar, "+="><identifier, id# = 2><delimiter, ";">

<intliteral, value = 0><delimitar, ",">

図 1.5: 字句解析の出力

ID = ID ILIT ; while ( ++ <= ) ID += ID ;=

ID ILIT

文の並び

・・・

図 1.6: 構文解析の出力

ID = ID ILIT ; while ( ++ <= ) ID += ID ;=

ID ILIT

文の並び

・・・

op=assigntype=int

op=assigntype=int

op=preincrementtype=int

op=letype=int

op=add&assigntype=int

図 1.7: 意味解析の出力

Page 20: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

20 第 1章 言語処理系の位置づけと概観

clrl a6@(-8) ← xに相当する領域を 0にclrl a6@(-4) ← iに相当する領域を 0に

L14:

addql #1,a6@(-8) ← iを 1増やすcmpl #100,a6@(-8) ← 100と比較jge L15 ←以上ならループを出るmovl a6@(-8),d0 ← iを d0に取り出してaddl d0,a6@(-4) ← xに足し込むjra L14 ←ループの頭へ戻る

L15:

図 1.8: 目的コードの例

"i"

"x" 1

2

integer

integer

局所変数

局所変数

-4

-8

名前 ID# 型 種別 番地

図 1.9: 記号表の例

直接機械語を生成する場合もある.図 1.8に先のソースコードのアセンブリ言語

形式での出力例を示しておく.

記号表: ソースコードに現れる名前等の所在,使われ方,型,主記憶上の位置などの

情報を記録するデータ構造.これらの情報は主として字句解析部と意味解析部に

より登録され,意味解析部とコード生成部によって参照される.先のソースコー

ドを処理した時記号表に蓄積される情報の例を図 1.9に示す.

図 1.4のようなコンパイラ構造は必要最小限の成分しかもたない簡単なものであるが,

目的コードの効率より翻訳速度が重要な場合に多く用いられる.

一般にコンパイラにおいては記号表以外の成分は「直列に」接続されていて,ソー

スコードの各部分はこれらの成分を順番に通過しながら目的コードに変換されていく.

そのためこれらの成分をフェーズないし段とよぶ.これに対し,記号表は各フェーズ

からの情報を受動的に蓄えるのが役目であり,フェーズとはよばれない.フェーズ間

の情報の受け渡し方としては次の 2 つがある.

Page 21: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1.4. コンパイラの諸成分 21

ソースコード

字句解析 構文解析 意味解析 中間コード 生 成

記号表

最適化パ ス

中間コード

コード生成

目的コード

図 1.10: 最適化コンパイラの構成例

a. あるフェーズの生成物をファイルまたは主記憶内のデータとして蓄え,そのフェー

ズが終った後でこのデータを入力として次のフェーズを開始する.

b. 例えば,字句解析部は構文解析部が次のつづりを必要とするつど呼び出され,次

のつづりが認識できるまでちょっとだけソースコードを読み進む,というように

各フェーズがこまぎれに (インタリーブされて)協調しながら動く.

複数のフェーズをインタリーブにより結合したものをパスとよぶ.例えば図 1.4のコ

ンパイラは 4つのフェーズから成っているが,これを全て 1つのパスにまとめた 1パ

スコンパイラとすることもできる.その場合は,構文解析がつづりを必要とするとき

はサブルーチンとして字句解析を呼び出し,また 1つの文が認識されるごとに意味解

析を呼び,引き続いてその文のコードを生成するためコード生成部を呼び出す,とい

う形をとる.

または,字句解析と構文解析は同様にインタリーブして実行するが,その結果は構

文木の形で蓄え,次にこれをたどりながら意味解析とコード生成を並行して行う,と

いう 2パス構成も自然な方法である.一般にパス数が少ない方が中間生成物の操作に

要するオーバヘッドが小さいので,翻訳速度を高めやすい.しかし,処理の内容によっ

ては,あるフェーズの処理を最後まで行った後でないと次のフェーズが始められず,そ

こでパスを分けざるを得ない場合もある.

図 1.4のように意味解析後ただちに目的コードを生成するようなコンパイラでは高

度な最適化 (コードの改良)は行いにくい.本格的に最適化を行う場合には,図 1.10の

Page 22: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

22 第 1章 言語処理系の位置づけと概観

ように意味解析の後で最適化の操作に適した中間コードを生成し,これに対して最適

化を施す.最適化フェーズは実際には種々の最適化を行う多数の小さなフェーズから

成ることが多い.これらの最適化フェーズはそれぞれ入力も出力も同じ中間コード形

式である.そこで,目的コードの品質より翻訳速度を重視するときは最適化フェーズ

を実行せずただちにコード生成に進むように切り換えることも可能である.

ここまでに取り上げてきたコンパイラの各フェーズはおおまかに

• 解析部 — ソースプログラムから文の構造や型その他の情報を抽出する部分

• 生成部 — 解析部の結果をもとに目的コードを作り出す部分

に分けられる.字句解析,構文解析,および意味解析までが解析部に相当し,その後

段が生成部となる.また,コンパイラが複数パス構成になっている場合,概ね解析部

に相当するパス (群)をフロントエンド,それ以降の部分をバックエンドとよぶ.例え

ば図 1.10の構成では中間コード生成前と後でパスが分かれるので中間コード生成まで

がフロントエンドとなる.

また,フロントエンドは主としてプログラミング言語に依存する部分,バックエン

ドは主として目的機械に依存する部分であると考えることもできる.このため,異な

る目的機械用のコードを出すようにコンパイラを改造する場合にはバックエンドのみ

を変更すればすむ.逆に,複数の言語のコンパイラが 1つのバックエンドを共有して

いる例もある.

1.5 コンパイラの周辺

図 1.4,図 1.10において「ソースコード」「目的コード」と記したものが具体的に何で

あるかもコンパイラの構成によって変化し得る.例えば C言語では別のソースファイ

ルからの取込み,字面での置換え,条件による翻訳部分の選択などを字句解析より前

のプリプロセッサ (pre-processor)とよぶ別フェーズで処理し,その出力がコンパイラ

の入力となる.特定の言語とは独立にこの種の前処理専用につくられた処理系もあり,

その場合はマクロプロセッサ (macro processor)とよばれる.

目的コードのあり方もコンパイラの構成によって様々である.例えばコンパイル&ゴー

型とよばれる処理系では,コード生成部がいきなり主記憶上に実行可能な機械語コー

Page 23: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1.5. コンパイラの周辺 23

プリプロセッサ

ソースコード プリプロセス済みコード

コンパイラ

再配置可能 コード

リンカ

ライブラリ

実行形式ファイル

取込み用ソースコード

図 1.11: コンパイラの周辺

ドを作り出し,翻訳後ただちにコードの先頭に分岐して実行を開始する.この方法は

コードをファイルに書き出す必要がなく,実行開始までの待ち時間が短いという利点を

もつ.他方,この方式のコンパイラは部分的に翻訳したコードを保存する機能をもた

ないことが多く,実行に必要な全てのコードを毎回一括して翻訳しなければならない.

コンパイル&ゴー型でない言語処理系では通常,実行可能な機械語コードを得るた

めにはリンカ (連携編集プログラム)とよばれるプログラムを使用する.この場合,コ

ンパイラの出力は機械語そのものではなく,再配置可能コード (relocatable code)と

よばれる形式をとる.再配置可能コードには機械語命令の情報に加えて,そのコード

に含まれている手続きの入口点や広域変数などの名前とコード中の位置,およびその

コード中で参照されているが,定義は他の再配置コード中にあるような入口点や広域

変数などの名前とそれらを参照している箇所,などの情報が含まれている.

実行形式を得るためには図 1.11に示すように,複数の翻訳出力をリンカによって結

合する.リンカは複数の再配置可能コードファイルを順次読み込んで詰め合せながら

各命令の番地を決定し,またファイルごとの定義と参照を互いに結び付ける働きも行

う.このような構成を採用することで次のような利点が得られる.

• プログラムを複数のモジュールに分割して開発し,それらを個別に翻訳すること

ができる.そうすれば,モジュールの変更を行っても (他のモジュールとの接続

部分に変化を及ぼさない限りは),そのモジュールのみを翻訳し直すだけですむ.

Page 24: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

24 第 1章 言語処理系の位置づけと概観

• プログラマが作成するモジュールに加えて,その言語仕様に標準として含まれて

いる手続きや,明示的には呼び出されないが実行時の下請けとして必要となる手

続きなどを実行時ライブラリとしてあらかじめ用意しておくこともできる.

• リンカは実行時ライブラリ組込みに便利なように,全オブジェクトを組み込んだ

時点で未解決の参照 (参照はあるがそれを定義した再配置可能コードファイルが

見つからない入口点や広域変数)については,その定義を含む再配置可能コード

を自動コールライブラリとよばれるファイル群から取り込んできて組込む.そこ

で多数のモジュールを共有する複数のプログラムを作成する場合には,共有され

るモジュール群も自動コールライブラリの形で保持することで,リンカに対する

繁雑なモジュール組込み指示を省略できる.

また,コンパイラが直接再配置可能オブジェクトを出力する代りにアセンブリ言語

コードを出力し,アセンブラがこれを再配置可能オブジェクトにするという構成も可

能であり,UNIX上のコンパイラに多く見られる.この方法ではいったんテキストファ

イルを生成し,それを再度アセンブラで解析するという手間がかかるが,アセンブリ

コードを書き出す部分はアーキテクチャが変っても比較的共通性があるためコンパイ

ラが移植しやすくなり,また必要ならアセンブリコード上で変更を施せる (例えば組込

みシステムやオペレーティングシステム中核部などで通常の呼出し規約に従わない場

所について必要な修正を機械的に行う等)という利点ももつ.

最後に,図には記さなかったが,実行形式ファイルを実際に主記憶に読み込んで実行

を開始させることも必要である.そのようなプログラムはローダとよばれる.ローダ

はオペレーティングシステムの内部の機能として組み込まれていることが多いが,リ

ンケージエディタの機能を兼ねた独立のプログラムである場合もある 1.

1.6 インタプリタの諸成分

図 1.12にインタプリタの構成例を示す.これは図 1.4とよく似ているが,ただし取り

込んだプログラムを解釈実行する部分が加わっている.また,プログラムは頭から順

番に実行したら終りではすまないので,プログラム全体の情報を何らかの形で主記憶

内に蓄えて (内部形式コードと記したところ)任意の順で参照できるようにする必要が1UNIX には ld(ローダ) という名のプログラムがあるが,機能的にはリンケージエディタに相当する.

Page 25: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1.6. インタプリタの諸成分 25

解釈実行

内部形式コード

コード生成意味解析構文解析字句解析

ソースコード

変数領域記号表

 入出力バッファ等

図 1.12: インタプリタの構成例

ある.加えて,プログラムの実行に伴い変数のアクセス 2 や入出力が起きるのでそれ

らの機能も合せて保持していなければならない (雲形はこれらの記憶領域を意味する).

また前節で述べた実行時ライブラリに相当するものも実行部分の機能の一部として含

まれる.

内部形式コードは,高速な解釈実行のためには,なるべく簡潔で解釈しやすい形式

をもち,変数のアクセスも名前の文字列よりは番号や番地などただちに所在位置が決

められる形で指定されている必要がある.これらの性質は実は機械語に近いものであ

り,したがって内部形式とは解釈実行に便利なような,仮想機械語 (virtual machine

language)であるということになる.このような形式をとる場合には解釈実行フェー

ズより前の部分は普通のコンパイラとさほど変らない 3.処理系によっては内部形式

コードをファイルに出力し,解釈実行部分はこのファイルを読み込む独立のプログラ

ムになっている場合もある.この場合には一度翻訳した結果を何回も実行することが

できる.

一方,機械語に近い状態まで翻訳してしまうと翻訳時間が長くなり,ソースプログ

ラムの情報が利用しにくくなるなど,インタプリタならではの利点は減少する.そこ

で内部コード生成を経ず,意味解析が終った後の抽象構文木をもとに解釈実行を行う

2本書では値を読み出すだけのときには「参照する」,読み出しと更新の双方がある場合には「アクセスする」と記す.

3翻訳の都合に合わせて仮想機械語を設計できるという利点はある.

Page 26: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

26 第 1章 言語処理系の位置づけと概観

インタプリタも存在する.このような構成は,実行途中でソースコードの一部が動的

に変更できるなど,一括して変換してから実行開始するのにそぐわない言語機能にも

対応できる.

さらに,変数の型などが動的に変化する言語ではあらかじめ意味解析を行えないの

で,構文解析が終った段階の抽象構文木を用いて実行を行う方式が取られる.Lispイ

ンタプリタなどでは S式 (Lispプログラムの表現形式.詳しくは付録を参照のこと.)

を読み込んだままの状態で主記憶に蓄え,これを解釈実行するため,括弧の対応以外

の構文誤りは実行してみるまで検出できない.したがってこれらは字句解析の結果を

用いてただちに実行に入るものと考えられる 4.

もっと極端な構成として,ソースコードをそのまま文字列の形で蓄え,それを解釈

実行する処理系もある.この方式は,例えばループがあればその中のコードは同じ字

句解析を繰り返し行うことになるので,実行速度の点からは不利である.コマンドイ

ンタプリタや問合せ言語など,比較的各命令が高レベルで繰返し処理などが現れる頻

度の少ない言語の処理系に多く見られる.

実行速度と柔軟性を両立させる 1つの手法として,ソースコードの各部分ごとに,最

初に実行する直前にその部分を低レベルの表現に翻訳して保存するという方法もある.

そのようにすれば繰り返し実行されている間は低レベル表現になっているため効率良

く実行できる.ひとたびソースコードや変数の型などに変化が起きた場合には低レベ

ル表現を捨ててしまい,改めて必要になったときに再翻訳すればよい.

1.7 練 習 問 題

1-1. 読者の関わっている計算機システムの周辺で,いわゆるプログラミング言語では

ないが「言語」として捉えるのが適切なデータを探してみよ.また,それを処理

するソフトウェアはプログラミング言語処理系の技術を応用してつくられている

か.もしそうなら,そのためどのような利点/欠点が生じているか.もしそうで

なければ,そのようにしたらどのように良い/悪い点が生じると思うか.

1-2. 読者の周りにある「規則性をもったものの並び」(文字や記号の並びでもその他の

4Lisp でもコンパイルすれば当然これらの誤りは検出される.一方,動的な型の扱いなどは実質的にインタプリタと同じ機能を呼び出すようなコードに翻訳される.

Page 27: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

1.7. 練 習 問 題 27

物体の並びでもよい)を 1つ選び,その並び方の規則を正確に記述してみよ.ま

た,その規則とそのものが表している意味との対応関係も記述してみよ.

1-3. 読者の周りにあるプログラミング言語処理系をできるだけ多く列挙し,それらの

特徴や差異を表にまとめてみよ.それらの特徴のうち,その処理系が実行系/変

換系であることに起因する事柄は何か.それら以外の特徴は何に起因していると

思うか.

1-4. 読者が使うことのできるコンパイラを 1つ選び,簡単なソースプログラムを入力

とし,どのような目的コードが生成されるか観察せよ (アセンブリコード形式の

出力が指定できると楽であるが,機械語や再配置可能コードを解読するのもそう

大変ではない).さらに,最適化を行わせたときは目的コードがどう変化するか

まず予測し,その後実際に観察してみよ.

1-5. 読者が使うことのできるコンパイラを 1つ選び,その内部がどのように構成され

ているかを資料などで調べてみよ.多くのコンパイラは目的コードの他にも翻訳

時の統計情報や各種リスティング,中間コードなどを出力する機能をもっている

ので,そのような出力をできるだけ多く収集してみよ.ソースプログラムを変化

させたときそれらの出力にどのような変化が見られるか検討してみよ.

1-6. 読者がふだん使っているシステムでは,コンパイラ本体のほかに再配置可能コー

ドや実行形式コードを操作するツール (ソフトウェア)としてどのようなものが

あるか調べ,実際にそれらを使ってみよ.

1-7. 主プログラムと 2~3個のサブルーチンから成る簡単なプログラム (言語は何で

もよい)を作成し,それらを別々のファイルに分けて個別に翻訳した後リンカで

まとめて実行形式ファイルにしてみよ.さらに,サブルーチンの方はライブラリ

ファイルにして,自動的に組み込むようにしてみよ.もしサブルーチンの中にシ

ステムが提供しているルーチンと同じ名前のものがあったら何が起きるかも試し

てみよ.

Page 28: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス
Page 29: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

29

第2章 形 式 言 語

形式言語理論は言語の構文を定義する理論的枠組として,言語処理系と密接な関係を

もっている.特に,近年における字句解析や構文解析などのコンパイラのフロントエ

ンド部分の開発を助けてくれるツールの発展は,これらの理論とは切り離して考える

ことができない.本章では形式言語理論の中から,引き続く章で出てくる事柄の理解

に必要な部分を取り出し,簡潔に説明する.

2.1 形式言語の諸定義

まず,どのような言語でもそれを組み立てるもととなる文字ないし記号 (の集まり)が

なければ成立しない.形式言語ではこれをアルファベットとよぶ.

定義 有限個の記号から成る空でない集合 V をアルファベットとよぶ.V の要素を並

べて得られる記号列を語とよぶ.

V の要素を 0個以上並べて得られる語の全体を V ∗,1個以上並べて得られる語の全体

を V + と記す.また長さ 0の列を εと記す.したがって,V ∗ = V + ∪ {ε}である.例

えば V = {a, b}のとき,V ∗ = {ε, a, b, aa, ab, ba, bb, aaa, aab, abb, baa, . . .}となる.そ

して,言語は次のように定義される.

定義 V ∗の任意の空でない部分集合 Lを V の上の言語,Lの要素を言語 Lの文とよぶ.

例えば上の例で L = {a, b, aab}とすれば,これら 3つの語だけが言語 Lの文である.

しかし「正しい文を列挙する」やり方では有限個の文をもつ Lしか定義できない.一

方,例えば「aがいくつか並び,続いて bがいくつか並ぶもの」という言語を考える

Page 30: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

30 第 2章 形 式 言 語

と,これは V ∗の部分集合であり,かつ無限の要素をもつ.これを自然言語に頼らずに

定義するにはどうしたらよいか.それには「V ∗の要素中でこのような規則にかなうも

の」という形で言語を定義する.そのような「規則」を表現するのに,自然言語の代

りに生成文法というものの力を借りることができる.

定義 T,N を互いに共通部分をもたない,有限な記号の集まり,P を「α → β」(た

だし α ∈ (T ∪N)+,β ∈ (T ∪N)∗)の形をした要素の有限集合,S をN の 1要

素としたとき,4つ組 G = (T,N, P, S)を生成文法とよぶ.

ここで T の要素はプログラミング言語などで言えば if,x,:=などプログラムの字面

上に現れるつづり (トークン)に相当し,終端記号ないし端記号 (terminal symbol)と

よばれる.一方N の要素はプログラムの字面には現れないが,その構造を説明するの

に都合がいいような概念ないし構文要素 (「文」,「代入文」,「変数」など)に対応する

もので,非終端記号ないし非端記号 (non-terminal symbol)とよばれる. そして S は

「プログラム」に相当するもので,出発記号 (start symbol)とよばれる.最後に P は

生成規則 (production rule)とよばれ,導出に用いられる.

定義 u = xαy,v = xβy かつ α → β ∈ P のとき,u から v が単導出 (one-step

derivation)できるという (u ⇒ vと記す).u = u0 ⇒ u1 ⇒ . . . ⇒ un = v(n ≥ 0)

のとき uから vが導出 (derivation)できるという (u ⇒∗ vと記す).

そして,文法Gを用いて T 上の言語 L(G)は次のように定義される.

L(G) = {x ∈ T ∗|S ⇒∗ x}

すなわち,出発記号 Sから始めて次々に P の規則を適用していって生成される記号列

の中で,T の要素のみから成るものの集合が L(G)である.これが P を生成規則,G

を生成文法とよぶ理由である.生成文法を用いて前出の「aがいくつか並び,続いて b

がいくつか並ぶもの」という言語を定義するには次の生成規則を用いればよい.

P = {S → aAbB,A → aA,A → ε,B → bB,B → ε}

例えば「aabb」という列は次のようにしてこの P により導出できる.

S ⇒ aAbB ⇒ aaAbB ⇒ aabB ⇒ aabbB ⇒ aabb

Page 31: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

2.1. 形式言語の諸定義 31

AやBは最後は εに置き換って消えることに注意.以下では当面 T の要素を英小文字,

N の要素を英大文字,出発記号を S で表す.

生成文法に対して,各生成規則の形に応じて次のようなクラス分けを行う.

タイプ 0文法: 上の定義と同じ.つまりもっとも一般的な場合.

タイプ 1文法: 各生成規則において,左辺の記号列の長さは右辺の記号列の長さを超

えないもの.これは実は各生成規則が

mAn → mtn,A ∈ N, t ∈ (N ∪ T )+,m, n ∈ (N ∪ T )∗

の形をしている,というのと等価である.タイプ 1文法を文脈依存文法 (context

sensitive grammer,CSG)ともよぶが,これは後者の書き方をした場合に非端記

号 Aが置き換えられるかどうかはその周囲 (つまり文脈)に何があるかによって

決まることによる.

タイプ 2文法: 各生成規則の左辺が 1個の非端記号から成る.つまり各生成規則が

A → t, A ∈ N, t ∈ (N ∪ T )∗

の形をしているもの.タイプ 2文法を文脈自由文法 (context free grammer,CFG)

とも言うが,これは文脈依存文法と対比して,Aは周囲に何があろうと (文脈に

依存せず)いつでも tに置き換えてよい,という性質があるからである.

タイプ 3文法: 各生成規則が

A → aまたは A → aB,A,B ∈ N, a ∈ T

の形をしているもの.タイプ 3文法のことを正規文法 (regular grammer)とよ

ぶ.正規文法によって定義される言語はそれと等価な言語を表現する正規表現

(regular expression)が存在する,という性質があることから正規言語 (regular

language)とよばれる (正規表現については本章の後の方で解説する).

Page 32: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

32 第 2章 形 式 言 語

S A B

a

a b

b

図 2.1: aaa...bbb...に対応する有限オートマトン

2.2 文法と言語の認識/解析

生成文法では Gを与えて L(G)の要素を何でも適当に作り出すのは容易である (適用

できる P の規則を順に試していけばよい).しかし言語処理系ではその逆,すなわち

T+の要素 t(ソースプログラム)を与えて,それが確かに L(G)に属するかどうかを判

定すること,さらには S からどのように規則を適用して tが生成されるかを決めるこ

とが必要である.一般に前者 (判定)を行うプログラムを L(G)の認識器 (recognizer),

後者 (構造の決定)を行うプログラムを L(G)の解析器 (parser)とよぶ.

前掲の生成文法のクラスにおいて,後に述べたものほど認識器/解析器をつくるのが

容易である.例えば「aがいくつか,続いて bがいくつか」という文法の例はタイプ

2(文脈自由文法)であったが,これを次のように書き換えるとタイプ 3(正規文法)にで

きる (同一の言語を定義する生成文法は一般に複数存在し得ることに注意).

P = {S → aA,A → aA,A → bB,B → bB,B → ε}

図 2.1に○と◎と矢印から成るグラフを示したが,これは先の正規文法の各非端記

号が○や◎に,生成規則による置換りが矢印に対応したものとなっている.例えば

「S → aA」という規則は S の○から Aの○へ aのラベルがついた矢印に対応してい

る.また,εが単導出できる場合にはその非単記号は◎で表す 1.

次に○と◎をプログラム中の gotoで飛んでいけるラベルに対応させ,まず Sのラベ

ルから実行開始し,各ラベルの箇所で「次の記号」を読んで,それが aだったら aの

矢印に従って Aのラベルに飛ぶ,というプログラムを構成する.このプログラムに入

力を与えて実行させ,もしどこかで入力に対応するラベルが見つからなければ入力は

L(G)の要素ではない.◎のラベルに到着すれば,入力は L(G)の要素であり,これま

1A → a の形の規則があった場合には名なしの◎を用意し,A の○からそこへ a のラベルのついた矢印を描く.

Page 33: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

2.3. 文脈自由文法とその記法 33

でにたどった矢印はそれぞれ P の規則に対応していたので,それらを覚えておけば構

造もわかったことになる 2.

このような○と◎とラベルつき矢印から成るグラフをオートマトン (automata,自

動機械の意)とよぶ.そして,○と◎のことを状態 (state),最初にたどり始める状態

を初期状態,◎の方を最終状態,ラベルつき矢印を遷移 (transition)とよび,状態の数

が有限のものを有限オートマトンとよぶ.ここで示したように,正規文法からは常に

対応する (ということは文法が規定する言語を認識/解析する)有限オートマトンが作

り出せ,有限オートマトンは計算機プログラムに変換できるので,結果として正規言

語を認識/解析するプログラムが得られることになる.

一般的なプログラミング言語について言えば,その定義には正規文法では制約が強

過ぎるため,文脈自由文法を使用するのが普通である.文脈自由文法に対しても,そ

の大部分について,4章で述べるやり方によって効率良い解析器が作り出せることが知

られている.一方,タイプ 0文法やタイプ 1文法の場合にはそれを解析するアルゴリ

ズムは存在するが,認識する列の長さの 3乗に比例した計算時間を要するため,それ

を用いてプログラミング言語の構文を記述することはほとんどない.

2.3 文脈自由文法とその記法

ここまででは文法の記号を全て英字 1文字で表してきた.しかし,プログラミング言

語を記述する際には各文法記号に「意味のある名前」をつけたくなる.そこで,以下

では文脈自由文法を書き記すやり方を次のように改めることにする.

• 各記号を,その意味をよく表すような名前を用いて書き表す.

• 端記号については,「""」または「’’」中にソースファイルに現れる表現を直接

書くことで表すこともある.

• 矢印「→」の代りに「=」を用いる.また「ε」の代りに何も書かないか,または

「nil」と書く.(キーボードにある文字だけで文法を書き表すため.)

• 同一の左辺をもつ規則はまとめて,右辺を「|」で区切って並べて書く.

この方式で先に出てきた「aがいくつか,bがいくつか」を書いたものを示しておく.2ある○から同じラベルをもつ矢印が 2 個以上出ていて行き先が一意でない場合は 3 章で扱う.

Page 34: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

34 第 2章 形 式 言 語

Start

SeqA SeqB

SeqA

a a b b

SeqB

図 2.2: 構文木の例

Start = SeqA SeqB

SeqA = "a" SeqA | nil

SeqB = "b" SeqB | nil

このような記法をその発明者の名前を取って Backus Naur Form (BNF)とよぶ 3.

次にこのようにして記した文法と実際の入力の対応関係を示す記法について考える.

1つの方法は導出の系列を逐一記すことであるが,非常に長々しく,記号列のどこをど

の規則により置き換えたのかわかりづらいという欠点がある.この問題を解決する方

法の 1つは,導出を構文木によって表すことである.構文木とは (1)出発記号を根に

もち,(2)非端記号を中間の節にもち,(3)端記号を葉にもち,(4)各節から葉の方向へ

向かう枝はその節を左辺とする導出に対応するような木構造のグラフである.先の文

法での「aabb」の導出に対応する構文木を図 2.2に示す.ただし,構文木では導出列

と比べて落ちている情報がある.例えばこの構文木は

Start ⇒ SeqA SeqB ⇒ ”a” SeqA SeqB ⇒ ”a” ”a” SeqA SeqB

⇒ ”a” ”a” SeqB ⇒ ”a” ”a” ”b” SeqB ⇒ ”a” ”a” ”b” ”b” SeqB

⇒ ”a” ”a” ”b” ”b”

Start ⇒ SeqA SeqB ⇒ SeqA ”b” SeqB ⇒ SeqA ”b” ”b” SeqB

⇒ SeqA ”b” ”b” ⇒ ”a” SeqA ”b” ”b” ⇒ ”a” ”a” SeqA ”b” ”b”

⇒ ”a” ”a” ”b” ”b”

のどちらに対応しているとも考えることができる.そこで次の定義を行う.

定義 導出の各ステップにおいて,記号列に含まれ非端記号のうち最も左側にあるもの

を常に置き換える導出を最左導出 (leftmost derivation),最も右側にあるものを

常に置き換える導出を最右導出 (rightmost derivation)とよぶ.3ただしもともとの BNFでは非端記号は名前を<>で囲んで表し,端記号は「""」で囲まずにそのまま書

いていた.また=は::=を用い,nil という書き方はなかった.いずれにしても原理は同じことである.

Page 35: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

2.4. 正規文法と正規表現 35

Expr

Expr

Ident Ident Ident+ +

Expr

Expr

Ident Ident Ident+ +

図 2.3: 曖昧な文法の構文木

先の導出系列は前者が最左導出,後者が最右導出である.コンパイラで用いる解析ア

ルゴリズムは例外なくこのどちらかの導出を扱う.このいずれかであると決まれば,構

文木と導出系列は 1対 1で対応させられる.ところで,文法によってはある 1つの文

に対し構文木が 2つ以上存在することもある.例えば次の文法を見てみよう.

Expr = Expr "+" Expr | Ident

この文法で「Ident + Ident + Ident」を導出することができるが,その構文木は図

2.3のように 2つ存在する.このような文法を「曖昧である」と言う.反対に,任意の

文に対して常に構文木が 1つだけしか存在しないような文法を「曖昧でない」と言う.

通常,構文解析のアルゴリズムでは曖昧でない文法のみを扱う.

2.4 正規文法と正規表現

正規言語は文脈自由言語の特殊な場合なので,BNFで記述できる.しかし正規言語の

場合は正規表現 (regular expression)とよばれる,よりコンパクトな記法で表すことが

一般的である.正規表現およびそれが表す言語とは次のようなものである.

1. 「ε」は正規表現である.これは空列を表す.

2. aが任意の終端記号のとき,「a」も正規表現である.これは aそのものを表す.

3. α,βが正規表現であれば,「αβ」も正規表現である.これは αが表す列の後に β

が表す列を連結したものを表す.

4. αが正規表現であれば,「α∗」も正規表現である.これは αが表す列を 0回以上

反復したものを表す.

Page 36: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

36 第 2章 形 式 言 語

ただし,表現の曖昧さを除くため適宜 ()を使用してよい.例えば,「aがいくつか,続

いて bがいくつか」を正規表現で表すと「aa ∗ bb∗」となる.正規文法と正規表現の表

現能力は等しく,一方で記述された言語は他方でも記述できることが知られている.ま

たしたがって,正規表現を有限オートマトンに変換することもできるが,これについ

ては 3章で述べる.本書では正規表現の場合も先のBNFと同様,読みやすさのため=

で名前を定義することを許し,終端記号の方は""で囲むようにする.加えて αα∗(1回

以上の反復) を α+,(α | ε)(あってもなくてもよい)を [α]で表す.

2.5 練 習 問 題

2-1. 本文の例題の類似品として,「aがいくつか並び,続いて b が aと同じ個数だけ並

んだもの」という言語を規定する文脈自由文法を書け.

2-2. 上の言語を正規文法で規定することはできない.それを確認するため,正規文法

で書こうと試みよ.納得したら,なぜ不可能なのかを自分なりの言葉で説明せよ

(上の言語に対応する有限オートマトンが構成できないことを説明してもよい).

2-3. 上の言語でもし aや bの並ぶ個数に上限があれば今度は正規文法で表すことが可

能である.最大 3個として試せ (有限オートマトンを構成してもよい).

2-4. P = {S → aSBC, S → aBC,CB → BC, aB → ab, bB → bb, bC → bc, cC →

cc}という文脈依存文法により定義される言語はどんなものか.また,この言語

が文脈自由文法では書けないことを自分なりの言葉で説明せよ.

2-5. 読者の周りにある「規則性をもったものの並び」を選び,その並び方の規則を生

成文法によって記述してみよ.

2-6. 読者の好みの年月日日時の記法を取り上げ,それに対応する正規表現を構成せよ.

まずは簡単のため 13月とか 25時のような変なものも許すものでよい.つづいて

段々厳密なものに直せ.もしうるう年にも対処するつもりなら,年は去年,今年,

来年,さ来年の 4つだけでよいことにする.

Page 37: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

37

第3章 字 句 解 析

字句解析はコンパイラの一番最初のフェーズであり,したがってプログラマが書いた

ソースファイルが最初に通過する部分である.その仕事そのものは複雑ではないが,細

部の設計が処理系の使い勝手や効率を左右するような部分でもある.本章では字句の

定義方法から始めて,字句解析の理論的裏付けと実際について解説する.

3.1 字句定義の正規表現と有限オートマトン

字句解析部はソースプログラムの文字の並びを走査し,意味のあるかたまり (つづり,

ないしトークン — token)に切り分ける.つづりの種類としては,識別子 (変数名,手

続き名などの一般の名前のこと),予約語 (特別な意味をもつ名前),演算子,定数 (整

定数,実定数,文字定数…)などがある.また,つづり間の空白や注釈など以降の処理

に必要ない部分を読み飛ばすことも字句解析の役割である.

IConst = [ Sign ] Digit+

RConst = [ Sgin ] Digit+

( Expt [ Sign ] Digit+ | Dot Digit+ [ Expt [ Sign ] Digit+ ])

Sign = "+" | "-"

Expt = "E" | "e"

Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

Dot = "."

図 3.1: 数値定数を定義する正規表現

字句解析の処理を行うには,各つづりの定義が厳密に定まっている必要がある.字句

の定義の場合は (つづり中にまた別のつづりが含まれることはないので),その記述は正

Page 38: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

38 第 3章 字 句 解 析

DigitDigitIConst

Sign

nilRConst

ExptDot

DigitDigit

Sign

nil

Sign

nil

DigitDigit

Expt

DigitSign

nil

Digit

Digit

Digit

図 3.2: 図 3.1に対応する有限オートマトン

規文法で十分であり,正規表現によって表すのが適当である.例として整定数 (IConst)

と実定数 (RConst)を定義する典型的な正規表現を図 3.1に示す.

正規表現による記述の利点は,それを有限オートマトンに変換することで,効率よ

い字句解析器を自動的に生成できるということである.図 3.1の定義を有限オートマ

トンに変換したものを図 3.2に示す.これらのオートマトンによってある文字列が実定

数であるかどうかを判定できるが,実際に行いたいのは入力の文字の並びを順に見て

いって「どの種のつづりがあったか」を知ることである.それには,図 3.3のように

「本当の」初期状態を付加し,そこから各つづりを認識するオートマトンの初期状態に

向かう空遷移 (空の入力列に対応する遷移)を加えればよい.合せて,各最終状態にそ

れはどのつづりに対応しているかの情報をつけ加える.しかし,図 3.3のようなオー

トマトンをそのままプログラムに変換して字句解析器とすることはできない.なぜな

ら,初期状態から出ている多数の空遷移のうちどれを選んでいいかは簡単にはわから

ないからである (例えば識別子などは次の 1文字が英字かどうかでそちらへ行くかど

うか決められるが,整定数と実定数の場合はずっと先まで見ないと区別できない).こ

のように次の状態が一意に決まらない有限オートマトンを非決定性有限オートマトン

(non-deterministic finite automata,NFA)とよぶ.図 3.2のオートマトンも実はNFA

である (空遷移を進むかどうかは常に非決定的なので).これに対し,次の状態が一意に

決まっているオートマトンを決定性有限オートマトン (determinisitc finite automata,

DFA)とよぶ.前章で述べた,容易にプログラムに変換できる有限オートマトンとは

実はDFAのことである.しかし悲観するには及ばなくて,NFAをDFAに変換する方

Page 39: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.2. 正規表現から非決定性有限オートマトンへの変換 39

...

IConst

RConst

IDent

nil

nil

nil

図 3.3: つづり認識のための有限オートマトン

法が知られている.その前に,まず正規表現を NFAに変換する方法から述べる.

3.2 正規表現から非決定性有限オートマトンへの変換

図 3.2の非決定性有限オートマトン (NFA) は図 3.1の正規表現から手で構成したが,

空遷移をたくさんつくってもかまわなければこれを機械的にやるのはごく簡単である.

そのやり方を図 3.4に示す.まず初期状態と最終状態を用意する.次に,文字に対応

する正規表現の場合,初期状態から最終状態へのその文字による遷移をつくればよい.

これ以外の場合は全て既存のNFAを空遷移でつなげていく.例えば列 XYZに対応す

る NFAでは正規表現 X,Y,Zに対応する NFAをつくり,それぞれ前のものの最終

状態から次のものの初期状態への空遷移をつなげる.全体の初期状態からはXの初期

状態への空遷移,Zの最終状態からは全体の最終状態への空遷移をつくる.他のもの

も同様である.なお一般の NFAでは最終状態は複数個有り得るが,この方法でつくっ

ている限り最終状態も初期状態同様 1個だけである.

原理は以上だが,大きな例を手でつくるのは大変だし,かといって小さな例題だけ

やっていても面白くない.計算機の助けを借りることにして,以下では手順の記述と

して Lispのプログラムを示すことにする (何をやっているかの解説は適宜加える).

最初に図 3.1の規則のうち,文字クラス (数字とか符号とか) の定義は別に扱うこと

にして,どの名前はどの文字集合に対応しているかを広域変数*cdefs*にリストのリ

ストとして保持する.加えて全ての文字のリストも*anychar*という広域変数に保持

する.

Page 40: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

40 第 3章 字 句 解 析

C

X Y Z

X|Y

X Y Z

C

X

Y

[X]

X+

X

X

X

X*

図 3.4: 正規表現から NFAへの変換

(setq *cdefs*

’((digit #\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)

(sign #\+ #\-)

(expt #\e #\E)

(dot #\.)))

(setq *anychar*

’(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\+ #\- #\e #\E #\.))

次に作成するNFAの表現であるが,N1,N2,…という記号を順次生成し,それぞれを

状態に対応させ,状態遷移の情報をその記号の属性値として保持することにする.こ

のやり方はあまりモジュール性が高いとは言えないが Lisp処理系に向かっていろいろ

試してみるには便利である.このためプレフィクスとなる文字列と番号を与えて記号

を生成する関数 newsymを次のように定義する.

(defun newsym (s n &aux x)

Page 41: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.2. 正規表現から非決定性有限オートマトンへの変換 41

(setq x (intern (format nil "~A~A" s n))) (setf (get x ’num) n)

x)

番号を numという属性に保存しているのは,あとで状態に対応する記号のリストを

番号の昇順に並べるところがあるためである.加えて遷移と最終状態の情報を属性値

として保持することとし,それらの値をアクセスするマクロを用意する.

(defmacro n-final (x) ‘(get ,x ’final))

(defmacro n-trans (x) ‘(get ,x ’trans))

属性値 finalには,この状態が最終状態であれば認識できたつづりを表す記号,そう

でなければ nilを入れる.属性値 trans には遷移の情報を

((次状態 文字...) (次状態 文字...) ... )

の形で入れる.文字が 0個のときは空遷移を表す.何も遷移をもたない新しい状態を

1つつくる関数 newsymは次の通り.

(defun nfa-newstate (&aux n)

(setq n (newsym "N" (incf *nfa-count*))) (push n *nfa-states*)

(setf (n-final n) nil) (setf (n-trans n) nil)

n)

つまり,新しい状態記号を生成しそれを全状態のリストを保持する変数*nfa-states*

に追加し,最終状態の情報も遷移のリストも nilに初期設定する.次に,ある状態か

ら別の状態に空遷移を張る関数 nfa-linkを示す.

(defun nfa-link (n1 n2) (push (list n2) (n-trans n1)))

すなわち,状態 n1の遷移情報に n2への空遷移を追加する.

これでだいたい準備ができたので正規表現からNFAをつくるわけだが,簡単さのた

め正規表現そのものの代りにそれを S式表現に直したものを入力する.その対応規則

は次のようにする.

X Y Z ... (seq X Y Z ...)

X | Y | Z | ... (alt X Y Z ...)

[X] (opt X)

X+ (rep+ X)

X* (rep* X)

Page 42: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

42 第 3章 字 句 解 析

この規則に従い図 3.1の正規表現から変換した S式表現を変数*ldefs*に入れておく.

(setq *ldefs* ’(

(iconst (seq (opt sign) (rep+ digit)))

(rconst (seq (opt sign) (rep+ digit)

(alt (seq expt (opt sign) (rep+ digit))

(seq dot (rep+ digit)

(opt (seq expt (opt sign) (rep+ digit)))))))))

最後に,以下の記述で (Cや Pascal風に)

(for 変数名 リスト 本体...) (while 条件式 本体...)

というのを使いたいので,これらをマクロとして定義する.

(defmacro for (v l &rest body) ‘(dolist (,v ,l) ,@body))

(defmacro while (c &rest body) ‘(loop (if ,c nil (return)) ,@body))

さて,次に示す関数 ldefs-to-nfaが NFAへの変換をつかさどる関数である.

(defun ldefs-to-nfa (&aux s1 s2)

(setq *nfa-states* nil)

(setq *nfa-count* 0)

(setq *nfa-start* (nfa-newstate)) ; (1)

(for x *ldefs* ; (2)

(multiple-value-setq (s1 s2) (nfa-make (cadr x))) ; (3)

(nfa-link *nfa-start* s1) (setf (n-final s2) (car x)))) ; (4)

(1)まず NFAの全状態のリスト*nfa-state*と状態数*nfa-count*をリセットし初期

状態をつくって*nfa-start*に入れる.あとは NFA全体というのは,図 3.3に示した

ようにあらゆる字句要素のNFAを並列につないだものだから,(2)*ldefs*の各要素に

ついて,(3)まず部分NFAをつくり,(4)続いて初期状態から部分NFAの初期状態へ

の空遷移を張り,部分NFAの最終状態に最終状態を表す記号をセットする.なお,(3)

で呼んでいる nfa-makeは正規表現の S式を受け取って対応する NFAをつくり,その

初期状態と最終状態の 2つを値として返す (本章の方法でNFAを構成していくときは

初期状態も最終状態は常に 1つだけであることに注意).その内容は次の通り.

Page 43: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.2. 正規表現から非決定性有限オートマトンへの変換 43

(defun nfa-make (r &aux s1 s2 s3 s4 s5)

(setq s1 (nfa-newstate)) (setq s2 (nfa-newstate)) ; (1)

(cond

((symbolp r)

(push (cons s2 (cdr (assoc r *cdefs*))) (n-trans s1))) ; (2)

((eq (car r) ’seq)

(setq s5 s1)

(for x (cdr r) (multiple-value-setq (s3 s4) (nfa-make x))

(nfa-link s5 s3) (setq s5 s4)) ; (3)

(nfa-link s5 s2))

((eq (car r) ’alt)

(for x (cdr r) (multiple-value-setq (s3 s4) (nfa-make x))

(nfa-link s1 s3) (nfa-link s4 s2))) ; (4)

((eq (car r) ’opt)

(multiple-value-setq (s3 s4) (nfa-make (cadr r)))

(nfa-link s1 s3) (nfa-link s1 s2) (nfa-link s4 s2)) ; (5)

((eq (car r) ’rep+)

(multiple-value-setq (s3 s4) (nfa-make (cadr r)))

(nfa-link s1 s3) (nfa-link s4 s2) (nfa-link s4 s3)) ; (6)

((eq (car r) ’rep*)

(multiple-value-setq (s3 s4) (nfa-make (cadr r)))

(nfa-link s1 s2) (nfa-link s1 s3) (nfa-link s4 s2)

(nfa-link s4 s3))) ; (7)

(values s1 s2)) ; (8)

(1)新しい初期状態と最終状態をつくる.そして場合分けに進み,(2)正規表現が記号

の場合には*cdefs*からその名前を検索して初期状態から最終状態への遷移をつくる.

(3)seqで始まるリストの場合にはその各要素を直列につないでいく.(4)altで始まる

リストの場合にはその各要素について初期状態からの空遷移と最終状態への空遷移を

つくる.(5)opt,(6)rep+,(7)rep*の場合には図 3.4に示したように空遷移を配線す

る.いずれにしても部分正規表現に対しては自分自身を再帰的に呼び出して部分NFA

を作成して参照する.(8)最後に最初につくった 2つの状態が初期状態と最終状態なの

で,これらを多値として返す.

プログラムの実行結果を次に示す.見ての通り,これだけで状態数が 57 もあるので

全部を掲載することはあきらめ,整定数に対応する辺りだけ結果を表示させた.また

それを図示したものを図 3.5に示す.

Page 44: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

44 第 3章 字 句 解 析

N1 N2 N4

N6 N7Sign

N5 N8

DigitN10 N11

N9 N3

N12 ...

IConst

図 3.5: 生成された NFA(一部)

>(ldefs-to-nfa)

NIL

>*nfa-count*

57

>(for x ’(n1 n2 n3 n4 n5 n6 n7 n8 n9 n10 n11 n12)

(format t "~% ~A ~A ~A" x (n-final x) (n-trans x)))

N1 NIL ((N12) (N2))

N2 NIL ((N4))

N3 ICONST NIL

N4 NIL ((N5) (N6))

N5 NIL ((N8))

N6 NIL ((N7 + -))

N7 NIL ((N5))

N8 NIL ((N10))

N9 NIL ((N3))

N10 NIL ((N11 0 1 2 3 4 5 6 7 8 9))

N11 NIL ((N10) (N9))

N12 NIL ((N14))

3.3 非決定性有限オートマトンから決定性有限オートマト

ンへの変換

次に非決定性有限オートマトン (NFA)を決定性有限オートマトン (DFA)に変換する

が,その基本的なアイデアは次のようなものである.NFAではある状態である 文字が

来たときに進む「次の状態」が複数あった.そこで,「NFAの状態の集合」をそれぞれ

新たに 1つの状態であると考えて有限オートマトンをつくる.すると,ある「もとの

Page 45: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.3. 非決定性有限オートマトンから決定性有限オートマトンへの変換 45

x

x

x

y

図 3.6: 簡単な NFAの例

x

x

x

y

x

x

x

y

x

x

x

y

yx

x

図 3.7: NFAの状態集合を状態とする DFA

NFAの状態の集合」においてある文字が来たときに進むことができる「もとの NFA

の状態の集合」は (集合として)1つだから,結果的に DFAがつくれる.

例えば,図 3.6のオートマトンは xが奇数個続いて最後に yがある語のみを受理す

るが,最初の xが来たとき行く先が 2つあるから NFAである.次に図 3.7を見ると,

この有限オートマトンの各状態は先の NFAの状態の集合 (斜線で塗られたもの)から

成っている.初期状態は左の箱,つまり元のNFAの初期状態だけが塗られたものであ

る.ここで xが来ると,元のNFAで行ける両方の状態が塗られた状態,つまり真ん中

の箱へ来る.ここで yが来ると,元のNFAで行けるのは右上の状態だけだから,それ

だけが塗られた右側の箱に来る.一方 xが来た場合は元の NFAで行けるのは初期状

態だけだから,左側の箱へ戻る.これで,無事全ての入力について行き先が一意に定

まった有限オートマトン,つまり DFAが構成できた.最終状態は元のNFAでの最終

状態を含んだもの,つまり右側の箱だけとなる.

この手順を実現するために NFA上で働く関数を 2つ用意する.まず,NFA である状

態に来たということはそこから空遷移で進める状態にも行けるということだから,そ

のようなあらゆる状態 (空遷移による閉包— closure)を集める計算が必要になる.こ

れを行うのが関数 nfa-closureである.

(defun nfa-closure (rem &aux n1 s1 done)

(while rem ; (1)

Page 46: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

46 第 3章 字 句 解 析

(setq n1 (pop rem)) (push n1 done) ; (2)

(for x (n-trans n1)

(and (null (cdr x)) (not (member (car x) done))

(not (member (car x) rem)) (push (car x) rem)))) ; (3)

(sort done #’(lambda (x y) (< (get x ’num) (get y ’num))))) ; (4)

引数として NFAの状態集合 (をリストとして表したもの)remを受取り,(1)それが

空でない間,(2)そこから 1つ状態を取り出し,それを doneに追加する.そして (3)

そこから出ている各遷移について,もしそれが空遷移であってその行き先がまだ done

にも remにもなければ,それを remに追加する.(4)remが空になったら doneが求む

結果だが,後の都合のため番号順に整列してから返す.

もう 1つはNFAの状態の集合に対して,そこから文字 cにより遷移できる状態の集

合を計算する関数 nfa-gotoである.

(defun nfa-goto (s c &aux o)

(for x s ; (1)

(for y (n-trans x) ; (2)

(and (member c (cdr y)) (not (member (car y) o)) ; (3)

(push (car y) o)))) ; (4)

o) ; (5)

まず oを nilにしておき,(1)渡されたNFA の各状態について (2)その各遷移を調べ,

(3)そこに文字 cがあり,その行き先がまだ oに入っていなければ (4)oにつけ加える.

(5)最後に oに入っているものが求める集合である.

次にDFAの状態の表現であるが,今度は名前は D1,D2…とし,各状態の情報はNFA

と同様で,ただしもとの NFAでの状態集合を保持する属性 nstatsを追加する.

(defmacro d-final (x) ‘(get ,x ’final))

(defmacro d-trans (x) ‘(get ,x ’trans))

(defmacro d-nstats (x) ‘(get ,x ’nstats))

NFAの状態集合 sを受け取り,対応する DFAの状態をつくる関数 dfa-newstate

は次の通り.

(defun dfa-newstate (s &aux d)

(setq d (newsym "D" (incf *dfa-count*))) (push d *dfa-states*)

(setf (n-final d) nil) (setf (n-trans d) nil) (setf (d-nstats d) s) ; (1)

(for x s (if (n-final x) (push (n-final x) (d-final d)))) ; (2)

d)

Page 47: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.3. 非決定性有限オートマトンから決定性有限オートマトンへの変換 47

(1)記号を生成して属性を初期設定するのはNFAのときと同様だが,(2) 最終状態の

情報は sに含まれているNFA状態に最終状態が含まれているかどうかによる.ところ

で,DFA状態が新しくつくられるのはこれまでに現れたことのない NFA状態集合が

現れたときだけなので,NFA状態集合 sを渡して対応する DFA状態があればそれを

返し,なければ nilを返す関数 dfa-lookstateを用意しておく.

(defun dfa-lookstate (s &aux o)

(for x *dfa-states* (if (equal (d-nstats x) s) (setq o x)))

o)

これは*dfa-states*を順に調べてすでにsと同じ状態集合をもったものがあるか探す

だけである.次に,DFAの状態 s1から s2に文字 cのラベルをもつ遷移を追加する関

数 dfa-linkを示す.

(defun dfa-link (c s1 s2 &aux x)

(setq x (assoc s2 (d-trans s1))) ; (1)

(if x (nconc x (list c)) (push (list s2 c) (d-trans s1)))) ; (2)

(1)すでに s1から s2への遷移が存在するかどうかまず調べ,(2)もし存在すればその

遷移ラベルの末尾に文字 cを追加するが,存在しなければ「(s2 c)」の形のリストを

遷移情報に追加する.以上で下請けが揃ったので,関数 nfa-to-dfaの定義を示す.

(defun nfa-to-dfa (&aux rem s1 s2 x)

(setq *dfa-states* nil)

(setq *dfa-count* 0)

(setq *dfa-start* (dfa-newstate (nfa-closure (list *nfa-start*))))

(push *dfa-start* rem) ; (1)

(while rem ; (2)

(setq s1 (pop rem)) ; (3)

(format t "~%Next: ~A ~A ~A" s1 (d-nstats s1) (d-final s1))

(for c *allchar* ; (4)

(setq x (nfa-closure (nfa-goto (d-nstats s1) c))) ; (5)

(cond ((null x)) ; (6)

((setq s2 (dfa-lookstate x)) (dfa-link c s1 s2)) ; (7)

((setq s2 (dfa-newstate x)) (dfa-link c s1 s2) ; (8)

(push s2 rem) (format t "~%New: ~A ~A" s2 x)))))) ; (9)

(1)まず広域変数群を初期設定してから変数 remに NFAの初期状態のみから成る集合

の閉包に対応する DFA状態 (これが DFAの初期状態となる)を入れておく.(2)次に

Page 48: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

48 第 3章 字 句 解 析

remが空でない間繰り返し,(3)remから 1つ DFA状態 s1を取り出し,(4)全ての文

字について (5) そのDFA状態から遷移し得るNFA状態集合の閉包を求め,(6)それが

空でなければ (7)すでに存在する DFA状態と一致するか調べ,存在すれば s1からそ

こへ文字 cによる遷移を追加する.(8)存在しなければ新しくDFA状態をつくりそこ

へ文字 cによる遷移を追加するとともに,(9)新しくつくった状態を remに追加する.

実行の様子を図 3.8に示し,結果のDFAを図 3.9に示す.ところで,図 3.9のDFAを

見ると確かに決定性にはなっているが,手でやればもっ と状態数を少なくできる.例

えば D4,D11,D10のところと D7,D9,D8のところはそっくり同じ形をしているので,

重ね合せてしまえそうである.実は任意のDFAについてそれと等価 (というのは,同

じ入力に対しては同じ最終状態に落ち着く,という意味)であり,かつ状態数が最小の

ものが 1つだけ存在することが知られている.その作り方については略した (練習問題

を参照されたい).

3.4 字句解析器生成系

ここまでに述べた有限オートマトンの原理を利用して実用的な字句解析器を自動生成

するツールが多数つくられている.その代表的なものとしてはUNIXに含まれている

字句解析器生成ツール lex,それと同一仕様でつくられコピーフリーなソフトとして配

布されている flexなどがある.これらはいずれも正規表現から有限オートマトンを生

成し,その構造を表の形で出力し,これと入力を走査しながらこの表をたどる (解釈実

行する)ドライバルーチンを組み合せることで字句解析器を構成する.これらのツール

で生成した解析器では,各正規表現ごとに,それを認識した際実行するプログラムの

断片が指定できる.

図 3.10に lexへの入力ファイルの例を示す.これは整定数と実定数に加えて名前,文

字列も認識する字句解析器を生成するためのものである.ここでは各つづりが認識さ

れた後認識したものが何であるかを打ち返すようになっている.最初の%%の前には定

義を記す.定義は

定義名 正規表現

の形をしているが,この正規表現はいわゆる「UNIX流の正規表現」であってこれまで

用いてきたものとやや異なる.まず「[文字...]」はかぎ括弧内の文字のどれか,「[^文

Page 49: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.4. 字句解析器生成系 49

>(nfa-to-dfa)

Next: D1 (N1 N2 N4 N5 N6 N8 N10 N12 N14 N15 N16 N18 N20) NIL

New: D2 (N3 N9 N10 N11 N19 N20 N21 N22 N24 N26 N36 N38)

New: D3 (N5 N7 N8 N10 N15 N17 N18 N20)

Next: D3 (N5 N7 N8 N10 N15 N17 N18 N20) NIL

Next: D2 (N3 N9 N10 N11 N19 N20 N21 N22 N24 N26 N36 N38) (ICONST)

New: D4 (N27 N28 N29 N30 N32 N34)

New: D5 (N39 N40 N42)

...

Next: D4 (N27 N28 N29 N30 N32 N34) NIL

New: D10 (N13 N23 N25 N33 N34 N35)

New: D11 (N29 N31 N32 N34)

Next: D11 (N29 N31 N32 N34) NIL

Next: D10 (N13 N23 N25 N33 N34 N35) (RCONST)

NIL

>(for x (reverse *dfa-states*)

(format t "~% ~A ~A ~A ~A" x (d-nstats x) (d-trans x) (d-final x)))

D1 (N1 N2 N4 N5 N6 N8 N10 N12 N14 N15 N16 N18 N20)

((D3 + -) (D2 0 1 2 3 4 5 6 7 8 9)) NIL

D2 (N3 N9 N10 N11 N19 N20 N21 N22 N24 N26 N36 N38)

((D5 .) (D4 e E) (D2 0 1 2 3 4 5 6 7 8 9)) (ICONST)

D3 (N5 N7 N8 N10 N15 N17 N18 N20) ((D2 0 1 2 3 4 5 6 7 8 9)) NIL

D4 (N27 N28 N29 N30 N32 N34) ((D11 + -) (D10 0 1 2 3 4 5 6 7 8 9)) NIL

D5 (N39 N40 N42) ((D6 0 1 2 3 4 5 6 7 8 9)) NIL

D6 (N13 N23 N37 N41 N42 N43 N44 N45 N46 N48)

((D7 e E) (D6 0 1 2 3 4 5 6 7 8 9)) (RCONST)

D7 (N49 N50 N51 N52 N54 N56) ((D9 + -) (D8 0 1 2 3 4 5 6 7 8 9)) NIL

D8 (N13 N23 N37 N45 N47 N55 N56 N57) ((D8 0 1 2 3 4 5 6 7 8 9)) (RCONST)

D9 (N51 N53 N54 N56) ((D8 0 1 2 3 4 5 6 7 8 9)) NIL

D10 (N13 N23 N25 N33 N34 N35) ((D10 0 1 2 3 4 5 6 7 8 9)) (RCONST)

D11 (N29 N31 N32 N34) ((D10 0 1 2 3 4 5 6 7 8 9)) NIL

図 3.8: DFAへの変換の実行例

Page 50: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

50 第 3章 字 句 解 析

D1 D2 D4

D3D5 D6 D7

D8

D9

D11

D10

0-9

0-9 0-9

0-9

0-9

0-9

0-9

0-9

0-9

E,e

"."+,-

+,-

0-9 E,e

+,- 0-9

図 3.9: 生成された DFA

L [A-Za-z_]

D [0-9]

DQuote \"

Escape \\

Dot \.

StrChar [^"\\]

AnyChar .

Ident {L}({L}|{D})*

IConst [+-]?{D}+

RConst [+-]?{D}+([Ee][+-]?{D}+|{Dot}{D}+([Ee][+-]?{D}+)?)

SConst {DQuote}({StrChar}|{Escape}{AnyChar})*{DQuote}

%%

{Ident} printf("<ident,%s>\n", yytext);

{IConst} printf("<iconst,%s>\n", yytext);

{RConst} printf("<rconst,%s>\n", yytext);

{SConst} printf("<sconst,%s>\n", yytext);

[\n\t" "] ;

%%

図 3.10: lexソースファイルの例

Page 51: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.4. 字句解析器生成系 51

字...] 」はかぎ括弧内の文字以外の文字のどれかを意味し,「X または空」は「X?」

で表す.また任意の 1文字を「.」で表す.(),+,*の用法は同じである.最後に定義

の参照は「{定義名 }」で表す.最初の%%の後にはパターンと動作の組を

正規表現 C言語の文

の組で表す.この例では識別子,整定数,実定数,文字列定数を認識するごとにそれ

を種別とともに<>で囲んで打ち出し,改行とタブと空白が来たら無視するようになっ

ている.なお,文が実行される際には配列 yytextにパターンとマッチした入力文字列

が蓄えられている.コンパイラの字句解析器として使用する場合にはこれらの印字動

作の代りに return文を用いて字句解析モジュールを呼び出した側につづりの種類を

返すようにするのが普通である.2番目の%%の後にはこの字句解析器と一緒に組み込

みたい Cのコードが書けるのだが,ここでは特に何も書いていない.この lexソース

の実行例を次に示す.

% lex sample.lex

% cc lex.yy.c -ll

% a.out

"this is a pen."

<sconst,"this is a pen.">

abc

<ident,abc>

123

<iconst,123>

123e10

<rconst,123e10>

123efg

<iconst,123>

<ident,efg>

^D

%

ソースファイル sample.lexを指定して lexを動かすと自動的に lex.yy.cというプ

ログラムが生成される.これを単独で動かす場合には-llというオプションつきでこ

のファイルを翻訳する.実行すると,確かに各種のつづりを正しく切りわけてくれて

いることがわかる.面白いのは,123e10というのは 1つの実定数だが 123efgという

のは整定数と名前が連続したものだと思われる点で,これについては次節で述べる.

Page 52: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

52 第 3章 字 句 解 析

3.5 字句解析の実際

前節の lexの動作例からもわかるように,有限オートマトンを用いた字句解析器は単

に有限オートマトンをたどっているだけではすまない.まず初期状態から入力の文字

を 1つずつ読みながら状態遷移を行うが,その途上で最終状態を通過するごとにそれ

を覚えておく.そしてこれ以上進めなくなった所で最後に通過した最終状態のつづり

を認識したものとして,それに対応する動作を遂行する.続いてまた初期状態から文

字を読み始めるが,ただし先の最終状態が進めなくなって止まったところより前であ

ればその先読んだ文字をもとに戻しておく必要がある.これは"123efg"という文字列

が来たとき,fまで読み進んではじめて 123が整定数であり eからは識別子だとわか

るような場合に必要だからである.また,進めなくなったところまでに 1つも最終状

態を通過していなければ認識不可能な入力でありエラーである.エラーの場合にはそ

の文字を読み捨てて初期状態から再開してもよいが,字句解析ではどんなつづりが来

ても一応認識し (全く使われない記号などはエラーというつづりにする),構文解析の

段階で統一して扱う方法もある.

空白,行替え,注釈などは一応認識して単に「無視する」動作を行えばよいが,必然

的に以降のフェーズではどのつづりがどの行にあったかの情報は失われてしまう.後

のフェーズでメッセージ等を出力する際に入力ファイルでの位置がわからないのは困

るので,各つづりに何行目にあったかを示す情報を付加しておくことも行われる.そ

れには改行が現れたら行のカウンタを増すなどの動作が必要となる.

識別子や各種の定数の場合にはその種別を返すだけでなく,入力文字列に対する処

理も必要である.例えば識別子であれば記号表を検索し,もし登録されていなければ

新たに登録し,いずれにしても構文解析や意味解析以降でその情報を参照できるよう

にしておく.また各種の定数の場合にはやはり定数表に登録しておき,あとでその値

が参照できるようにしておく.

識別子と関連して問題になるのは予約語の扱いである.予約語 (reserved words)と

は,まったく普通の識別子と同じような形をしているが言語表記上特別な意味をもち,

通常の名前としては使えないような語を指す.例えば多くの言語では ifや whileな

どを予約語としている.予約語を正規表現の枠内で扱おうとすると「英字で始まり英

字または数字が 0個以上並び,なおかつ ifでも thenでも...でもない」という正規表

現を書くことになり大変困難である.

Page 53: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.6. ハンドコーディングによる字句解析 53

これに対しては 2つの方法がある.1つはこれまでの識別子の定義と「iと fなら if」

という定義をかまわず併置することである.その場合当然できた有限オートマトンの

中には NFAの 2つの最終状態に同時に対応するものが現れるが,NFAの最終状態に

順位をつけておいて (もちろん ifの方を識別子より高くする),曖昧な場合には順位の

高いものを取ることにすればよい.lexにはそのような機能も備わっている.この方法

は全て有限オートマトンのみで統一的に扱える一方,有限オートマトンの状態数が大

きくなるという欠点がある.

もう 1つの方法は予約語の情報は有限オートマトンには含めず,識別子が認識でき

たあとで別の表を検索して予約語かどうか調べるというものである.言語の予約語の

数はそれほど多くないので,何らかの工夫を行って高速に検索を行うことは難しくな

い.この代替案として,識別子はどうせ記号表に登録するのだから記号表に予約語か

どうかの情報を入れてしまうということも考えられる.しかし,できるだけ字句解析

モジュールと他のモジュールの干渉を少なくするという意味ではあまり勧められない.

ここまででは,字句解析のための有限オートマトンの各遷移はソースプログラムに

現れる各文字によって直接ラベルづけされていた.しかし,多くの言語では,指数用

の「E」は別格として,文字「A」と「B」が字句解析のうえで違う扱いをされること

はないはずである (前述の,予約語を有限オートマトンによって認識する場合は除く).

したがって,直接文字そのものを扱う代りに「E以外の英字」「数字」といった文字ク

ラスを設けてその上で有限オートマトンを構成し,入力の各文字も文字クラス番号に

変換したうえで有限オートマトンをだどるようにすることもできる.これによって有

限オートマトンの大きさを節約することができる.特に,日本語を扱う場合には少な

くとも漢字部分についてはこのような処置が不可欠である.

3.6 ハンドコーディングによる字句解析

最後になったが,実は字句解析というのはそれほど「難しい」作業ということはない

ので,字句解析部を全て手で書く,というのも十分実用的な方法である.実際,lexな

どのツールによって生成された字句解析器は有限オートマトンを表現するデータ構造

を入力に従ってたどりながら動作する,いわばインタプリタの形になる.一方,手で

字句解析器を書いた場合には必然的に現在プログラム上のどこを走っているかが状態

に対応するので,実行速度の面ではこの方が有利である.

Page 54: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

54 第 3章 字 句 解 析

また,全てをプログラムとして書くわけだから,字句の認識と並行して様々な処理

を行わせることができる.例えば数字を読み進めるのと並行して整定数の値を計算し

たり,名前の各文字を読み進めながらそれを文字列領域にコピーしたりできる.生成

ツールに頼った場合にはこれらの文字はツールが定めた場所に蓄積され,つづりが認識

された時点で改めて値を計算したりコピーを行うことになるので,結局頭から 2回文

字列を処理することになる.ソースコード中の名前や数値の数は非常に多いので,こ

の差は結構無視できない.

ただし,この方法をとる場合には有限オートマトンがあまり複雑でないことが必要

なので,lexの場合のように予約語の認識を有限オートマトンによって行わせるのは困

難である.そこで前述のように,予約語はいったん識別子として認識され,その後で

簡単な表を引いて予約語かどうかを調べる方法が一般に使われる.

このように手で字句解析器を書く場合でも,どのようなものをつづりとして認識す

べきかを正確に規定することはどのみち欠かすことができない.したがって,字句を

正規文法や正規表現などで定義する,という道具立てはこのような場合にも十分役に

立つ.

3.7 練 習 問 題

3-1. 自分の知っている任意のプログラミング言語について,その字句定義を正規表現

(ないし正規文法)で書き表してみよ.特に,白い部分 (空白文字,タブ文字,改

行,注釈など)をきちんと扱うようにしてみよ.

3-2. 問題 3-1でつくった正規表現のうち手頃そうな部分を取り出し,手で NFAに変

換してみよ.直観的に行ってもよいし,3.3節で説明した手順によってもよい.

3-3. 問題 3-2でつくった NFAを手で DFAに変換してみよ.また,それが最小である

かどうか検討し,最小でないなら手で最小化してみよ.

3-4. CommonLispの処理系が使えるなら,本節に掲載した Lispのコードを打ち込み,

問題 3-3,3-4と同じことをプログラムにさせてみよ.結果としてできる有限オー

トマトンは手でつくったものと同じかどうか.

Page 55: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

3.7. 練 習 問 題 55

3-5. 最小でない DFAを最小 DFAに変換するには,原理的には次のようにする.(1)

まず DFAの状態をどうしても互いに区別しなければいけない集合にまとめる.

具体的には各つづりごとの最終状態の集合と,最終状態以外の全ての状態の集合

に分ければよい.(2)各状態集合ごとに全ての文字 cについて,そこに含まれて

いる各状態から文字 cによる遷移を調べ,行き先の状態集合が 1つでなければ行

き先の別により状態集合を分割する.(3)以上をこれ以上分割が起きなくなるま

で反復する.実際にこれを行う Lispコードを書いてみよ.

3-6. lex,flexその他の字句解析器生成ツールが使えるなら,それを用いて 3.5節に説

明した程度の例題を動かしてみよ.それで満足しなければ,問題 3-1で用意した

字句定義に対応する字句解析器を生成してみよ.

3-7. 3.5節の例題では「123efgが整数 123と識別子 efgになる」字句解析器ができ

てしまったが,そうではなくエラーになる (つまり整数と識別子が隣合っている

場合には 1個以上空白等がないといけない)字句定義にしてみよ.

3-8. 問題 3-6で生成させた字句解析器と同等の動作をする字句解析器のプログラムを

手でコーディングせよ.大変だったら,整定数と識別子だけということでもよい.

完成したら,それと問題 3-6で生成させたものとの効率を比較せよ.

Page 56: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス
Page 57: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

57

第4章 構 文 解 析

構文解析はコンパイラの中で単なる 2番目のフェーズ,というよりはだいぶ重要な位

置を占める.というのは,構文解析部はコンパイラの認識部の中枢であり,構文解析

部が各構文要素を認識するのに合せて種々の動作が駆動されるようにコンパイラ (また

は,少なくともそのフロントエンド部)を構成することが多いからである.本章では

ソースプログラムの構造を認識する各種の方式とその実装方法について解説する.

4.1 構文解析と構文木

文脈自由文法は,記述としてわかりやすく,かつ文法記述から効率的な構文解析器を

作り出せるため,プログラミング言語の記述手法として多く用いられる.本章でも文

脈自由文法による構文記述を前提として話を進める.まず例として図 4.1のような簡

単な言語を考える.解析部の出力としては,当面構文木を生成するものとし,

read x; read y; if(x > y) { z = x; x = y; y = z; } print x; print y;

なるプログラムが入力されたとして,これに対応する構文木を組み立ててみて頂きたい.

答えは,図 4.2のようになるはずである.ここで構文木を組み立てる過程は,概ね上

Program = StatList

StatList = Stat StatList | nil

Stat = Ident "=" Expr ";" | "Read" Ident ";" |"print" Expr ";" |

"if" "(" Cond ")" Stat | "{" StatList "}"

Cond = Expr "<" Expr | Expr ">" Expr

Expr = Ident | Iconst

図 4.1: ごく簡単な言語の文法

Page 58: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

58 第 4章 構 文 解 析

Stat

Stat

read x ; read y ; if ( x > y ) { z = x ; x = y ; y = z ; } print x ; print y ;

Exp Exp Exp

Stat Stat Stat

StatList

StatList

StatList

ExpExp

Stat

ExpExp

StatList

StatList

StatList

StatList

Program

Stat Stat StatCond

StatList

図 4.2: プログラムに対応する構文木

(根)の方から描いていくか,または下 (葉)の方から描いていくか,のどちらかであろ

う.文脈自由文法の解析アルゴリズムも同様に分類でき,前者を下向き解析 (top-down

parsing),後者を上向き解析 (bottom-up parsing)とよぶ.以下ではこれらの代表的手

法について解説する.

4.2 下 向 き 解 析

4.2.1 下向き解析とFirst/Follow

図 4.2の構文木を下向きに描く過程を考察する.まず構文木の根は出発記号 Program

に決まっている.次に Programを左辺にもつ規則は 1つしかないから,StatListが

根の直下の節となる.次であるが,ここで StatListは 2通りに置換できるので,その

どちらかを選択する必要がある.まず Stat StatListに置換していいか考える.その

場合,入力の最初に Statがないといけないが,具体的には最初の入力記号は"read"

である.一方 Statは"read" Ident ";"に置換できるので,こちらが正しそうである.

Stat StatListを選び,この Statを"read" Ident ";"に置換すると,これらが入力

の最初の 3つづりに対応する.次は先の Stat StatListの StatList以下の対応に戻

り,4つづり目の"read"以下とこれを対応させ…のように進めばよい.

Page 59: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.2. 下 向 き 解 析 59

これを見て「構文規則は有限だから可能なものを順にあてはめて入力との対応を検

査していけばよさそうだ」と思うかもしれないが,それでは困る.なぜならそれだと

「やってみてだめなら戻って別の枝を試す」(バックトラックする)ことになるから,入

力つづりの数を nとして,解析にかかる計算量のオーダがO(n)より大きくなってし

まう.長いソースプログラムを扱うコンパイラはそれでは実用にならない.

そうではなく,「StatListをどちらに置換するか」などの選択肢が現れたとき,先の

方まで試してみることなく正しい選択を行う必要がある.上の過程を振り返ると,Stat

StatListを展開して行った先が"read"...になるから,こちらの枝を選んだのだっ

た.そして,構文記号は有限個しかないので,全ての構文記号Aについて「展開して

いくとどんな端記号から始まり得るか」の集合 (これを First(A)と記す)をあらかじ

め計算しておける.合せて,各記号 Aごとに「その後に来ることができる端記号の集

合」(これを Follow(A)と記す)も計算しておく (その用途は後述する).

Firstと Followの計算を Lispで記述する.まず,変数*nonterms*と*terms*にそ

れぞれ非端記号と端記号のリストを入れ,変数*rules*に生成規則 (を S式で表したも

の)のリストを入れておく.

(setq *nonterms* ’(program statlist stat cond expr))

(setq *terms* ’(ident = semi read print if lpar rpar { } < >))

(setq *symbols* (append *nonterms* *terms*))

(setq *rules*

’((program statlist)

(statlist stat statlist)

(statlist)

(stat ident = expr semi)

(stat read ident semi)

(stat print expr semi)

(stat if lpar cond rpar stat)

(stat { statlist })

(cond expr < expr)

(cond expr > expr)

(expr ident)

(expr iconst)))

端記号のうち「(」,「)」,「;」は Lispの S式と注釈に使われるので,「lpar」,「rpar」,

「semi」という名前で表した.次に,各記号ごとに「端記号か」「非端記号か」「First

のリスト」「Followのリスト」を属性値としてもたせる.そのアクセス用マクロを用

Page 60: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

60 第 4章 構 文 解 析

意する.

(defmacro s-term (s) ‘(get ,s ’term))

(defmacro s-nonterm (s) ‘(get ,s ‘nonterm))

(defmacro s-first (s) ‘(get ,s ’first))

(defmacro s-follow (s) ‘(get ,s ’follow))

また,「制御変数を 1きざみで増やしながら/減らしながらループする」制御構文を使

いたいので,それも用意しておく.

(defmacro fromto (var from to &rest body)

‘(do ((,var ,from (1+ ,var))) ((> ,var ,to)) ,@body))

(defmacro rfromto (var from to &rest body)

‘(do ((,var ,from (1- ,var))) ((< ,var ,to)) ,@body))

最初の関数として,広域変数の初期設定を行った後 First/Follow を計算する関数

を呼び出す comp-first-followを示す.

(defun comp-first-follow ()

(for x *terms* (setf (s-term x) t)) ; (1)

(for x *nonterms* (setf (s-nonterm x) t))

(setq *symbols* (append *terms* *nonterms*)) ; (2)

(compfirst) (compfollow)) ; (3)

(1)記号ごとに属性 term/nontermを設定し,また (2)端記号と非端記号のリストを合

せたものを変数*symbols*に入れておく.その後で (3)compfirst,compfollowをこ

の順に呼ぶ.これらはいずれも,「記号 sが記号 First(x)/Follow(x)に含まれていなけ

れば追加するとともにその旨表示し,変化があったことを示す旗を立てる」という操

作を何箇所かで行うので,それをマクロとして用意した.

(defmacro addtofirst (x s c)

‘(if (not (member ,s (s-first ,x)))

(progn (format t "~% First(~A) <- ~A" ,x ,s)

(push ,s (s-first ,x)) (setq ,c t))))

(defmacro addtofollow (x s c)

‘(if (not (member ,s (s-follow ,x)))

(progn (format t "~% Follow(~A) <- ~A" ,x ,s)

(push ,s (s-follow ,x)) (setq ,c t))))

関数 compfirstは次の通り.

Page 61: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.2. 下 向 き 解 析 61

(defun compfirst (&aux x a c allempty (change t))

(for x *terms* (setf (s-first x) (list x)))

(for x *nonterms* (setf (s-first x) nil)) ; (1)

(for r *rules*

(if (null (cdr r)) (addtofirst (car r) nil change))) ; (2)

(while change ; (3)

(for r *rules*

(if (cdr r)

(progn

(setq x (car r))

(multiple-value-setq (a c) ; (4)

(compfirst-r x (cdr r) nil))

(if c (setq change t)) ; (5)

(if a (addtofirst x nil change))))))) ; (6)

(1)全ての端記号の Firstはそれ自身のみとし,全ての非端記号の Firstをとりあえず

空にする.次に (2)規則のうち右辺が空のものについて,Firstに nilを加える.(3)

続いて旗 changedが立っている間繰返しに入り,(4)各規則のうち右辺が空でないも

のについて下請け関数 compfirst-rを読んで処理させる.この関数は「右辺の最後ま

で全てが空列になり得る記号だったかどうか」と「変化があったかどうか」の 2つの

値を返すので,(5)変化があれば旗 changedを立て,(6)右辺全てが空列になり得るな

ら Firstに nilを加える.compfirst-rは次の通り.

(defun compfirst-r (x r change)

(for y r ; (1)

(for z (s-first y) (if z (addtofirst x z change))) ; (2)

(if (not (member nil (s-first y)))

(return-from compfirst-r (values nil change)))) ; (3)

(values t change)) ; (4)

(1)右辺の各記号 yを左から順に調べ,(2)その Firstの各要素 (ただし nilは別扱い)

を First(x)に追加する.(3)もし First(y)に nilが含まれていないなら,右辺のそこ

から先は First(x)に影響しないのでそこで終る.(4)ループを最後まで回ったときは右

辺全てが空列になり得ることになるので,その旨の値をもって帰る.一方,compfollow

は次の通り.

(defun compfollow (&aux (change t) x)

(for x *nonterms* (setf (s-follow x) nil))

Page 62: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

62 第 4章 構 文 解 析

(push ’eof (s-follow (car *nonterms*))) ; (1)

(while change ; (2)

(setq change nil)

(for r *rules* ; (3)

(if (compfollow-r (car r) (cdr r)) (setq change t)))))

(1)まず全ての非端記号の Followを空にし,出発記号 (*nonterm*のリストの先頭に

入っているものとした)の Followに入力終りの印 eofを追加しておく.(2)続いて旗

changeが立っている間,(3)各規則について下請け関数 compfollow-rを呼ぶ.この

関数は値としてどれかの記号の Followに変化があった場合 tを返すので,そのとき

は旗 changeを立てる.compfollow-rは次の通り.

(defun compfollow-r (x r &aux y change)

(fromto i 0 (- (length r) 1) ; (1)

(setq y (nth i r))

(if (s-nonterm y)

(for z (s-first (nth (1+ i) r))

(if z (addtofollow y z change))))) ; (2)

(rfromto i (1- (length r)) 0

(setq y (nth i r))

(if (s-term y) (return)) ; (3)

(for z (s-follow x) (addtofollow y z change)) ; (4)

(if (not (member nil (s-first y))) (return))) ; (5)

change) ; (6)

(1)規則の右辺の記号を左から順に最後の 1つ手前まで調べながら,(2) その記号が非

端記号であればその右隣の記号の Firstを (nilは除いて)この記号の Followに追加

する.(3)次に今度は右辺の記号を右端から左に向かって端記号に出合わない限り順に

走査し,(4)左辺の記号の Followをその記号の Followにも加える.(5)この記号が空

列になり得なければそこで走査を終るが,そうでなければ左隣の記号に進む.(6)最後

に変化があったかどうかの旗を返す.実行例を次にに示す.

>(comp-first-follow)

First(STATLIST) <- NIL

First(PROGRAM) <- NIL

First(STAT) <- IDENT

First(STAT) <- READ

...

Follow(STATLIST) <- EOF

Page 63: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.2. 下 向 き 解 析 63

Follow(STAT) <- IDENT

...

NIL

>(for x *nonterms* (format t "~% First(~A) = ~A" x (s-first x)))

First(PROGRAM) = ({ IF PRINT READ IDENT NIL)

First(STATLIST) = (IDENT READ PRINT IF { NIL)

First(STAT) = ({ IF PRINT READ IDENT)

First(COND) = (IDENT)

First(EXPR) = (IDENT)

NIL

>(for x *nonterms* (format t "~% Follow(~A) = ~A" x (s-follow x)))

Follow(PROGRAM) = (EOF)

Follow(STATLIST) = (} EOF)

Follow(STAT) = (} EOF { IF PRINT READ IDENT)

Follow(COND) = (RPAR)

Follow(EXPR) = (> RPAR < SEMI)

NIL

First(X)に nilが含まれるのはX が空列に対応し得ることを意味していたのに注意.

確かに Programや StatListは空であることができる.

ここまででは 1つの記号の Firstを扱ったが,後で必要になるので n 個の記号につ

いても First(X1X2 . . . Xn)を考え,それを計算する関数 seqfirstを用意する.

(defun seqfirst (l)

(cond ((null l) (list nil)) ; (1)

((not (member nil (s-first (car l)))) (s-first (car l))) ; (2)

(t (union (remove nil (s-first (car l))) ; (3)

(seqfirst (cdr l))))))

(1)空列の First は nilのみから成る集合である.次に (2)First(X1)に nilがなけ

れば,記号列の First は First(X1).さもなければ X1 が空列になり得るのだから,

(3)First(X1)から nilを除いたものと First(X2 . . .Xn)の和集合が答えである.

4.2.2 LL(1)解析器とLL(1)文法

First/Follow の値を用いて下向き解析手法を一般化する.この手法では解析用にス

タックを 1つ使用し,次の入力記号 (先読み記号— lookahead symbol —とよばれる)1

つだけを見ながら解析を進める.スタックに出発記号 1つだけがあり,入力列の最初

Page 64: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

64 第 4章 構 文 解 析

の記号が見えている状態から始めて,入力がなくなりスタックが空になるまで次のこ

とを反復する.

• スタックの先頭が端記号 tなら,先読み記号も同じ端記号 tのはずである.この

場合はスタックを取り降ろし,入力を読み進める.

• スタックの先頭が非端記号Aなら,それを左辺にもつ規則の右辺をβとして,先

読み記号がFirst(β) (nil ∈ First(β)なら First(β)∪Follow(A))に含まれてい

るような規則を選び,Aをその β で置き換える.

この解析手法による解析器はLL(1)解析器とよばれる.最初のLは左から右へ入力記号

列を 1回捜査するだけで解析する (left-to-right)こと,2番目の Lは最左導出 (leftmost

derivation)に対応する順で解析が進められること,(1)は先読み記号 1個だけを見て

解析が進むことを意味する.その手順を実現する関数 llparseを示す.

(defun llparse (p &aux s r e)

(setq p (append p ’(eof)))

(setq s (list (car *nonterms*))) ; (1)

(loop (format t "~% ~A : ~A" (car p) s)

(cond

((null s) (return (if (eq (car p) ’eof) ’ok nil))) ; (2)

((eq (car s) (car p)) (pop s) (pop p)) ; (3)

((s-term (car s)) (return nil)) ; (4)

(t (setq r (lllookup (car s) (car p))) ; (5)

(if (eq r ’error) (return nil))

(pop s) (setq s (append (cdr r) s)))))) ; (6)

(1)まず入力列 pの末尾に eofを追加し,スタック sには出発記号のみを入れておく.

次にループに入り,(2)まずスタックが空なら入力も eofのみが残っているべきで,そ

うなら ok,そうでなければ失敗の印として nilを返す.(3)スタックが空でなく,ス

タックの先頭と先読み記号が一致していれば入力を読み進むとともにスタックも 1つ

取り降ろす.(4)スタック先頭と先読み記号が一致していなくてなおかつスタックの先

頭が端記号のときは,入力が構文に一致していないので失敗である.(5)スタック先頭

が非端記号のときは,その非端記号を生成規則のどれかに従って書き換える.適切な生

成規則を探すのは関数 lllookupによる.(6)その結果が’errorなら失敗だが,そう

でなければスタックを 1つ取り降ろし,返された規則の右辺で置き換える.lllookup

は次の通り.

Page 65: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.2. 下 向 き 解 析 65

(defun lllookup (n i &aux f r o)

(for r *rules*

(setq f (seqfirst (cdr r))) ; (1)

(if (and (eq n (car r)) ; (2)

(or (member i f) ; (3)

(and (member nil f) (member i (s-follow n))))) ; (4)

(setq o (if o ’error r)))) ; (5)

o) ; (6)

(1)各生成規則を順に調べるが,その際まずその右辺の記号列全体の Firstを計算する.

そして,(2)左辺がスタック先頭と一致し,なおかつ (3)先読み記号が Firstに含まれ

るか,または (4)nilが Firstに含まれていて先読み記号がスタック先頭記号の Follow

に含まれているなら,その規則は適用可能である.(5)それを変数 oに覚えるが,ただ

し oが nilでないならすでに別の適用可能な規則が見つかっているので適用可能な規

則が複数あることになり,その印として oに errorを入れる.(6)全規則を調べ終っ

たら oを返す.実行例を次に示すが,以下の表示では:の左側が先読み記号,右側がス

タックの内容 (左側が先頭)である.

>(llparse ’(if lpar ident < ident rpar print ident semi))

IF : (PROGRAM)

IF : (STATLIST)

IF : (STAT STATLIST)

IF : (IF LPAR COND RPAR STAT STATLIST)

LPAR : (LPAR COND RPAR STAT STATLIST)

IDENT : (COND RPAR STAT STATLIST)

NIL

失敗しているので,結果を追跡する.Programを StatList で置き換え,StatList

を Stat StatListで置き換え,先読み記号が"if"だから Statを"if" "(" Cond ")"

Stat で置き換え,入力の"if"と"("を読み進める.ここまではよい.次に置き換える

べきものは Condであるが,Condを左辺にもつ規則は

Cond = Expr "<" Expr

Cond = Expr ">" Expr

の 2つで,右辺の Firstが同一であり,したがってどちらを選択すべきか決められな

い.ここで文法を少し修正し,新しい非端記号 RelOpを導入して

Page 66: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

66 第 4章 構 文 解 析

Cond = Expr RelOp Expr

RelOp = "<"

RelOp = ">"

とする.こうすれば Condの置換え先は 1つになり,RelOpの置換え先を探すときには

先読み記号は"<"の所まで来ているから問題ない.文法を直した実行例を示す.

>(llparse ’(if lpar ident < ident rpar print ident semi))

IF : (PROGRAM)

IF : (STATLIST)

IF : (STAT STATLIST)

IF : (IF LPAR COND RPAR STAT STATLIST)

LPAR : (LPAR COND RPAR STAT STATLIST)

IDENT : (COND RPAR STAT STATLIST)

IDENT : (EXPR RELOP EXPR RPAR STAT STATLIST)

IDENT : (IDENT RELOP EXPR RPAR STAT STATLIST)

< : (RELOP EXPR RPAR STAT STATLIST)

< : (< EXPR RPAR STAT STATLIST)

IDENT : (EXPR RPAR STAT STATLIST)

IDENT : (IDENT RPAR STAT STATLIST)

RPAR : (RPAR STAT STATLIST)

PRINT : (STAT STATLIST)

PRINT : (PRINT EXPR SEMI STATLIST)

IDENT : (EXPR SEMI STATLIST)

IDENT : (IDENT SEMI STATLIST)

SEMI : (SEMI STATLIST)

EOF : (STATLIST)

EOF : NIL

OK

一般にLL(1)解析器で解析できる文法のクラスをLL(1)文法という.逆に,lllookup

で置換え規則が一意に決まらないならその文法は LL(1)ではない.LL(1)構文解析を

実用に用いる場合は,全ての非端記号と端記号の組について lllookupの値を計算し

て表の形で記憶するので,文法が LL(1)でない場合は表の計算時点で検出できる.

文法が LL(1)でなくなる場合の 1つは先の例のように適用規則が一意に決まらない

場合であるが,他に A ⇒+ Aβ なる Aが存在する場合 (これを文法に左再帰性がある,

という)も,Aを展開して行くとまた Aになってしまい,無限に同じ規則の適用が続

くだけで解析が進まなくなるためLL(1)解析器で解析できない.文法が LL(1)でなく

ても,言語は同じままで文法を書き換えて LL(1)にできる場合がある.まず適用規則

Page 67: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.2. 下 向 き 解 析 67

が一意に決まらない場合には,先の例のように共通部分を「くくり出す」ように規則

を書き換えればよい.一方,

A → Aβ

A → γ

のような左再帰を含む規則があった場合,これから生成される言語は γββ . . . βの形に

なる.そこで生成規則を

A → γA′

A′ → βA′

A′ → ε

のように書き換えればよい.このような直接の左再帰でない場合でも,中間に現れる規

則を展開して埋め込むことで直接左再帰に書き換えてから同様に処理すればよい.書

換えで LL(1)にできない文法も存在するが,プログラミング言語の構文として現れる

ことはまれである.むしろ書換えで問題なのは,認識される言語は同じでも文法記述

が理解しにくいものとなり,また構文木に沿った後段の処理も記述しにくくなってし

まう点である.これらは文法による定式化の利点を大きく損なうので,それよりは後

述の LR(1)のような広いクラスの文法を使用する方が好ましい.

4.2.3 再帰下降解析器

先の LL(1)解析アルゴリズムは生成規則と Frist/Followを参照しながらスタック上で

記号列を次々に置き換えていく,という形になっていたが,これと同等の動作を再帰的

手続き呼出しを利用して行わせる解析器 (再帰下降解析器,recursive-descent parser)

も広く使われている.その概要は次の通りである.

• 各非端記号について,それに対応する手続きを用意する.

• 手続き本体はその非端記号を左辺にもつ生成規則群に対応し,先読み記号に応じ

て (First/Followを参照して)いずれかの規則を選択する.どの規則も選択でき

ない場合には構文誤りである.

Page 68: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

68 第 4章 構 文 解 析

• 選択した後は規則の右辺 X1X2 . . . Xn に呼応して n個の文を順次実行する.Xi

が端記号の場合は,手続きmatch(Xi)を呼び出す.match(T )は次の入力が T で

あることを確認し (そうでなければ構文誤り),入力を 1つ読み進める.またXi

が非端記号の場合にはそれに対応する手続きを呼び出す.

先の例の言語の文法 (LL(1)に直した方)に対応する再帰下降解析器を図 4.3に示す (非

端記号 Condは名前を cond1に変えた).以下に trace機能を使って,その実行経過を

示す (各手続きの戻り値には特に意味はない).

>(trace program statlist stat cond1 expr relop)

(PROGRAM STATLIST STAT COND1 EXPR RELOP)

>(rdparse ’(if lpar ident < ident rpar print ident semi))

1> (PROGRAM)

2> (STATLIST)

3> (STAT)

4> (COND1)

5> (EXPR)

<5 (EXPR IDENT)

5> (RELOP)

<5 (RELOP <)

5> (EXPR)

<5 (EXPR IDENT)

<4 (COND1 IDENT)

4> (STAT)

5> (EXPR)

<5 (EXPR IDENT)

<4 (STAT SEMI)

<3 (STAT SEMI)

3> (STATLIST)

<3 (STATLIST (EOF))

<2 (STATLIST (EOF))

<1 (PROGRAM (EOF))

OK

再帰下降解析は先の LL(1)解析器における非端記号の置換を手続き呼出しに置き換え

た形になっている.したがって,呼出しの実行系列を描くと構文木と同じ形になる.そ

のため再帰下降解析器は直感的にわかりやすく,手で構成することも容易なので,対

象言語の文法が LL(1)で無理なく書き表せる場合に多く用いられる.また,文法が完

Page 69: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.2. 下 向 き 解 析 69

(defun rdparse (p)

(setq *input* (append p ’(eof))) (catch ’error (program) ’ok))

(defun match (s)

(if (eq s (car *input*)) (pop *input*) (throw ’error ’error)))

(defun program ()

(case (car *input*)

(({ if print read ident eof) (statlist))

(t (throw ’error ’error))))

(defun statlist ()

(case (car *input*)

(({ if print read ident) (stat) (statlist))

((} eof))

(t (throw ’error ’error))))

(defun stat ()

(case (car *input*)

((ident) (match ’ident) (match ’=) (expr) (match ’semi))

((print) (match ’print) (expr) (match ’semi))

((read) (match ’read) (match ’ident) (match ’semi))

((if) (match ’if) (match ’lpar) (cond1) (match ’rpar) (stat))

(({) (match ’{) (statlist) (match ’}))

(t (throw ’error ’error))))

(defun cond1 ()

(case (car *input*)

((ident) (expr) (relop) (expr))

(t (throw ’error ’error))))

(defun expr ()

(case (car *input*)

((ident) (match ’ident))

((iconst) (match ’iconst))

(t (throw ’error ’error))))

(defun relop ()

(case (car *input*)

((<) (match ’<))

((>) (match ’>))

(t (throw ’error ’error))))

図 4.3: 図 4.1の文法に対応する再帰下降解析器

Page 70: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

70 第 4章 構 文 解 析

全な LL(1)ではない場合でも,記号表その他の情報を参照したり余分に先読みするな

どの手当てを組み込むことで対処しやすいという利点ももつ.

4.3 上 向 き 解 析

4.3.1 シフト-還元解析器

本節では構文木を下の方から組み立てていく上向き解析を扱う.上向き解析を行う構

文解析器は通常シフト-還元解析器 (shift-reduce parser)の形をとる.そこで具体的な

解析方式の説明に先立ち,シフト- 還元解析器の枠組について説明する.

シフト-還元解析器の基本的な道具立てを図 4.4に示す.解析には構文記号を積むス

タック (解析スタック)1つと,先読み記号 1個を使用する (より一般的には入力記号を

n個先まで見ることも考えられるが,動作の原理としては同じである).そして,解析

器の行う動作は必ず次の 2つのうちいずれかである.

• シフト — 先読み記号をスタックに積み,入力を進める.

• 還元 — 生成規則 A → αに対応して,記号列 αの長さ分スタックを取り降ろし,

代りに Aを積む.

ただしこれでは終りがないので,文法を出発記号 S を左辺にもつ生成規則が 1つだけ

になるよう修正し,その規則番号を 1番とする.そして還元時に規則番号が 1番だっ

たら解析を終了する.以下では文法がこの形になっているものとする.図 4.1の言語の

断片をシフト-還元解析器で解析する様子を図 4.5に示す.これからわかるように,シ

フト-還元解析器では入力は次々にスタックに移され,スタック上に 生成規則の右辺と

"if" "(" Cond ")"

"print"Ident ":" EOF

解析スタック

先読み記号(残っている入力)

シフト-還元 解析器

図 4.4: シフト-還元解析器の枠組

Page 71: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 71

"if"

"if" "("

"if" "(" Ident

"if" "(" Ident

"if" "(" Expr

")"

")"

"<"

"<""<"

"if" "(" Expr "<" Ident

"if" "(" Expr "<" Ident

"if" "(" Expr "<" Expr

")""if" "(" Cond

"if" "(" Cond ")"

"if" "(" Cond ")" "read" Ident

"if" "(" Cond ")" Ident ";"

"if" "(" Cond ")" ";" EOF

"if" "(" Cond ")" EOFStat

EOFStat

EOFStat StatList

EOFStatList

EOFProgram

"read"

"read"

"read"

Ident

解析スタック 先読み記号

図 4.5: シフト-還元解析器による構文解析

Page 72: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

72 第 4章 構 文 解 析

一致するものができたときにそれが左辺の非端記号に置き換る,という形で解析が進

んで行く.ここまでで「なるほど,スタック上に規則の右辺が現れたら還元,それ以

外ではシフトをすればいいのか」と思われたかもしれないが,それほど単純ではない.

上の例でも Condの中の Identはスタックに積まれた直後に Expに還元されているが,

一方"read"のあとの Identはそのまま残されている.これはもちろん"read"を還元

する規則の右辺が"read" Ident ";"であるからだが,ともかく右辺が現れたらすぐ還

元してよい,というものでないことはわかる.

このシフトと還元の選択を正しく行うところがシフト-還元解析器の「きも」であり,

その部分の構成法によって,順位文法に基づく解析器,演算子の強さに基づく解析器

など,様々なバリエーションがある.以下ではその中で最も広い範囲の文法に適用可

能かつ実用性も高い,LR解析器とよばれる一群の解析器について説明する.LRの最

初の Lは LLと同様「left-to-rightに 1回入力を捜査するだけで解析を行う」という意

味であり,一方 2文字目のRは「rightmost derivationの逆順の系列を生成する」こと

を意味している.逆順なのは上向き,つまり出発記号から遠いところから構文木をつ

くっていくためである.

4.3.2 LRオートマトンと項

先の Identを還元するかどうかは,先に"read"をシフトしたか"if","("をシフトし

たかによって選択が違っていた.「置かれた状態によって同じ入力に対する動作が変化

する」ことが問題であるが,この種の問題はオートマトンで表現するのが適している.

ここではLRオートマトンとよばれるものを使うが,それは字句解析のときの有限オー

トマトンとはだいぶ異なる.まず,LRオートマトンでは各状態が「構文規則の右辺の

どの位置にいるか」に対応する.これを表すために,構文規則の右辺の任意の位置に •

を記入して「現在いる位置」を表す.これを項 (item)とよぶ (より厳密には LR(0)項.

0は項の中に先読み記号の情報が含まれていないことを意味する).例えば,規則 Stat

→ "if" "(" Cond ")" Statからは 6つの項ができる (項は []で囲んで表す).

[ Stat → • "if" "(" Cond ")" Stat ]

[ Stat → "if" • "(" Cond ")" Stat ]

[ Stat → "if" "(" • Cond ")" Stat ]

[ Stat → "if" "(" Cond • ")" Stat ]

Page 73: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 73

[ Stat → "if" "(" Cond ")" • Stat ]

[ Stat → "if" "(" Cond ")" Stat • ]

先で必要になるので,まず項を Lispのデータ構造で表現し,全ての項をあらかじめ生

成したり,各項を表示する関数群を記しておく.先の First/Followの計算では必要な

かったが,以下では「どの規則」という情報が多く必要になるので,生成規則を記号

R1,R2…で表し,規則の左辺と右辺はその属性 lhs,rhsに入れるものとする.属性

をアクセスするマクロは次の通り.

(defmacro r-lhs (r) ‘(get ,r ’lhs))

(defmacro r-rhs (r) ‘(get ,r ’rhs))

そして生成規則群をこの表現に変換する関数 initrulesは次の通り.

(defun initrules (&aux x)

(setq *rule-count* 0) (setq *rule-list* nil) ; (1)

(for r *rules* ; (2)

(setq x (newsym "R" (incf *rule-count*))) ; (3)

(setf (r-lhs x) (car r)) (setf (r-rhs x) (cdr r)) ; (4)

(push x *rule-list*)) ; (5)

(setq *rule-list* (reverse *rule-list*))) ; (6)

(1)規則数のカウンタを 0にしてから,(2)各規則について (3)記号を生成し,(4)左辺

と右辺を属性値にセットし,(5)生成した記号を変数*rule-list*に追加する.(6)最

後に*rule-list*を逆向きにしているのは R1,R2…の順にしたいからである.

次に,LR(0)項であるが,これも記号 I1,I2…で表し,次の属性をもたせる.

(defmacro i-rule (i) ‘(get ,i ’rule)) ; 対応する生成規則名(defmacro i-lhs (i) ‘(get ,i ’lhs)) ; 左辺(defmacro i-pre (i) ‘(get ,i ’pre)) ; ・より前の部分(defmacro i-post (i) ‘(get ,i ’post)) ; ・より後の部分(defmacro i-next (i) ‘(get ,i ’next)) ; 「次の」項(defmacro i-str (i) ‘(get ,i ’str)) ; 文字列表現

つまり,•の位置をそれより前の部分と後の部分の 2つのリストに分けて保持するこ

とで覚える (実現しやすさのため,前の部分は逆順のリストにした).「次の」項とは,•

の位置が 1つ右にずれた項のことである.文字列表現は項を見やすく表示するための

ものである.1つの規則に対応する一群の項を生成する関数 newitemを示す.

Page 74: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

74 第 4章 構 文 解 析

(defun newitem (r pre post &aux x)

(setq x (newsym "I" (incf *item-count*))) (push x *allitem*)

(setf (i-rule x) r) (setf (i-pre x) pre) (setf (i-post x) post)

(setf (i-lhs x) (r-lhs r))

(setf (i-next x) (if post (newitem r (cons (car post) pre) (cdr post))))

(setf (i-str x) (format nil "[~A ~:A ~:A]"

(i-lhs x) (reverse pre) post))

x)

つまり •の右が空でなければ自分を再帰的に呼んで「次の」項をつくるとともにそれ

を覚える.これを用いて全 LR(0)項をあらかじめ生成する関数 inititemは次の通り.

(defun inititems ()

(setq *item-count* 0) (setq *allitem* nil)

(for r *rule-list* (newitem r nil (r-rhs r)))

(setq *allitem* (reverse *allitem*)))

4.3.3 LR(0)オートマトンの作成

準備ができたので,上向き構文解析のためのオートマトンをつくる話に戻る.字句解

析の決定性オートマトンでは,1つの状態は複数の正規表現のそれぞれ特定の場所に

「並行して」対応していた.ここでも同様なことを考える必要がある.例えば,

[ Stat → "if" "(" • Cond ")" Stat ]

という状態にいるということは,Condの直前にいるということだが,一方で

Cond → Expr "<" Expr

Cond → Expr ">" Expr

であるのだから,これは同時に

[ Cond → • Expr "<" Expr ]

[ Cond → • Expr ">" Expr ]

という状態にも「並行して」いることでもある.さらには

Expr → Ident

Expr → Const

Page 75: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 75

だったから,したがってこれらの状態にいるということは

[ Expr → • Ident ]

[ Expr → • Const ]

にもいることになる.このように「並行していることになる」項の集合を (LR(0)項集

合の)閉包 (closure)とよぶ.したがって,LRオートマトンの各状態は項の集合で,な

おかつ閉包になっている必要がある.引数として渡された項の集合 (リストで表す)の

閉包を計算する関数 closureを示す.

(defun closure (l &aux c i)

(while l ; (1)

(setq i (pop l)) (push i c) ; (2)

(if (i-post i)

(for j (collect (car (i-post i))) ; (3)

(if (and (not (member j c)) (not (member j l)))

(push j l))))) ; (4)

(sortsym c))

(defun collect (s &aux o)

(for i *allitem*

(if (and (eq s (i-lhs i)) (null (i-pre i))) (push i o)))

o)

(1)受けとった集合 lが空でない間,(2)lから項を 1つ取り出してそれをcに移し,(3)

その項の •の直後にある記号と同じものを左辺にもつ生成規則に対応し,なおかつ •

が右辺の先頭にある項を関数 collectにより集めてきて,(4)それらの中に lにも c

にもまだ入っていないものがあれば (あとで調べるため)lに追加する.

LRオートマトンの初期状態は,生成規則 1(出発記号を左辺にもつ唯一のもの)から

つくられ,•が一番左にある項の閉包となる.オートマトンの各状態も記号 S1,S2…

によって表すこととし,次の属性アクセス関数を用意する.

(defmacro s-items (s) ‘(get ,s ’items))

(defmacro s-goto (s) ‘(get ,s ’goto))

(defmacro s-red (s) ‘(get ,s ’red))

itemsは状態に対応する項の集合を保持する.あとの 2つはすぐ後で説明する.項

の集合から新しい状態を 1つつくる関数 newstateを示す.

Page 76: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

76 第 4章 構 文 解 析

(defun newstate (l &aux s)

(setq l (sortsym l))

(setq s (newsym "S" (incf *state-count*))) (push s *state-list*)

(setf (s-items s) l) (setf (s-goto s) nil) (setf (s-red s) nil)

s)

なお,項の集合をリストとして番号順に並べておくのは後で同じ集合が出てきたか

どうかを容易に判定できるようにするためである 1.

オートマトンの全状態を求めるには,初期状態から何らかの入力によって到達でき

る状態を全て求めればよい.そこで次に必要なのは「ある状態で,ある記号が来たら

どの状態に移行するか」を計算することで,これを行うのが関数 compgotoである.

(defun compgoto (s x &aux o)

(for i (s-items s) ; (1)

(if (and (i-post i) (eq (car (i-post i)) x))

(push (i-next i) o))) ; (2)

(if o (closure o))) ; (3)

(1)状態 sに含まれる全ての項のうち,(2)•の直後が記号 xであるようなものを集め,

(3)その閉包をとる.これを用いて,全状態を計算するとともに,各状態ごとに「どの

記号ならどの状態へ進むか」を属性 gotoに覚える関数 compstatesを示す.

(defun compstates (&aux l o d s0 s1)

(setq *state-count* 0) (setq *state-list* nil)

(setq s0 (newstate (closure (list (car *allitem*)))))

(setq l (list s0)) ; (1)

(while l

(setq s0 (pop l)) (push s0 o) (format t "~% Next: ~A" s0) ; (2)

(for x *symbols* ; (3)

(setq d (compgoto s0 x)) ; (4)

(cond

((null d) nil) ; (5)

((setq s1 (member d l :test #’sameset)) ; (6)

(push (list x (car s1)) (s-goto s0)))

((setq s1 (member d o :test #’sameset)) ; (7)

(push (list x (car s1)) (s-goto s0)))

(t ; (8)

1Lisp のリストはその要素を集めた集合として扱うこともできる.以下本書では要素の順序に意味がある場合に「リスト」,ない場合に「集合」と記す.

Page 77: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 77

図 4.6: compstatesの実行結果

(setq s1 (newstate d))

(push (list x s1) (s-goto s0)) (push s1 l)))))

(setq *state-list* (reverse *state-list*))) ; (9)

(1)まず広域変数を初期設定してから始め,最初の項 (出発記号を左辺にもつ唯一の規

則の最初に •がある)の閉包に対応する状態を出発状態として用意し,リスト lにこ

の状態を入れておく.次に lが空でない間,(2)lから状態を 1つ取り出して oに移す

とともに,(3)全ての記号 xについてそれぞれ,(4)この状態から xによって遷移する

状態に対応する項の集合を求める.(5)集合が空ならそのような遷移は不可能なので何

もしない.(6)(7)集合が lや oに含まれている状態のどれかと等しいなら,記号 xと

見つかった状態名とのリストをもとの状態の属性 gotoに追加する.(8)集合がすでに

あるどの状態とも等しくなければ,新しい状態を生成したうえで同様に属性 gotoに追

加するとともに,新しい状態を lに追加する.(9)これ以上新しい状態が増えなくなっ

たら終りだが,ここでも*state-list*が番号順になるように逆転しておく.なお,す

でにある状態かどうかを調べるには項のリスト cが状態 sの items属性に入っている

リストと等しいかどうかを調べる関数 samesetを使っているが,それは次の通り.

(defun sameset (c s) (equal c (s-items s)))

これらの関数により図 4.1の文法に対応する LR(0)オートマトンを構成させた結果を

図 4.6に示す.これをもとにオートマトンの図を描いたものを図 4.8に示す.中身の項

集合は量が多いので図 4.7に別途示した.

この LRオートマトンを用いた解析過程を手でシミュレートしてみる.今度は道具立

てとして,解析スタックと先読み記号に加えて「現在の状態」が必要で,解析スタック

には構文記号と LRオートマトンの状態を交互に載せる.まず状態 S1から開始し,入

力記号を"read",Ident,";",とシフトしながら S3,S24,S25と遷移すると同時に,

これらの記号と状態をそれぞれスタックに載せていく.S25まで来ると「行き止り」に

なってしまうが,この状態は生成規則 R6の一番最後に •をもった項から成っているの

で,R6に従って還元を行う.具体的には,R6 の右辺の記号数の倍 (状態と記号を対に

して載せたから)だけスタックを取り降ろし (S1だけが残る),左辺 (つまり Stat)を積

Page 78: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

78 第 4章 構 文 解 析

S1:[PROGRAM () (STATLIST)] S12:[STAT (IF LPAR) (COND RPAR STAT)]

[STATLIST () (STAT STATLIST)] [COND () (EXPR < EXPR)]

[STATLIST () ()] [COND () (EXPR > EXPR)]

[STAT () (IDENT = EXPR SEMI)] [EXPR () (IDENT)]

[STAT () (READ IDENT SEMI)] [EXPR () (ICONST)]

[STAT () (PRINT EXPR SEMI)] S13:[EXPR (IDENT) ()]

[STAT () (IF LPAR COND RPAR STAT)] S14:[STAT (IF LPAR COND) (RPAR STAT)]

[STAT () ({ STATLIST })] S15:[COND (EXPR) (< EXPR)]

S2:[STAT (IDENT) (= EXPR SEMI)] [COND (EXPR) (> EXPR)]

S3:[STAT (READ) (IDENT SEMI)] S16:[COND (EXPR <) (EXPR)]

S4:[STAT (PRINT) (EXPR SEMI)] [EXPR () (IDENT)]

[EXPR () (IDENT)] [EXPR () (ICONST)]

[EXPR () (ICONST)] S17:[COND (EXPR >) (EXPR)]

S5:[STAT (IF) (LPAR COND RPAR STAT)] [EXPR () (IDENT)]

S6:[STATLIST () (STAT STATLIST)] [EXPR () (ICONST)]

[STATLIST () ()] S18:[COND (EXPR > EXPR) ()]

[STAT () (IDENT = EXPR SEMI)] S19:[COND (EXPR < EXPR) ()]

[STAT () (READ IDENT SEMI)] S20:[STAT () (IDENT = EXPR SEMI)]

[STAT () (PRINT EXPR SEMI)] [STAT () (READ IDENT SEMI)]

[STAT () (IF LPAR COND RPAR STAT)] [STAT () (PRINT EXPR SEMI)]

[STAT () ({ STATLIST })] [STAT () (IF LPAR COND RPAR STAT)]

[STAT ({) (STATLIST })] [STAT (IF LPAR COND RPAR) (STAT)]

S7:[PROGRAM (STATLIST) ()] [STAT () ({ STATLIST })]

S8:[STATLIST () (STAT STATLIST)] S21:[STAT (IF LPAR COND RPAR STAT) ()]

[STATLIST (STAT) (STATLIST)] S22:[STAT (PRINT EXPR) (SEMI)]

[STATLIST () ()] S23:[STAT (PRINT EXPR SEMI) ()]

[STAT () (IDENT = EXPR SEMI)] S24:[STAT (READ IDENT) (SEMI)]

[STAT () (READ IDENT SEMI)] S25:[STAT (READ IDENT SEMI) ()]

[STAT () (PRINT EXPR SEMI)] S26:[STAT (IDENT =) (EXPR SEMI)]

[STAT () (IF LPAR COND RPAR STAT)] [EXPR () (IDENT)]

[STAT () ({ STATLIST })] [EXPR () (ICONST)]

S9:[STATLIST (STAT STATLIST) ()] S27:[STAT (IDENT = EXPR) (SEMI)]

S10:[STAT ({ STATLIST) (})] S28:[STAT (IDENT = EXPR SEMI) ()]

S11:[STAT ({ STATLIST }) ()]

図 4.7: LRオートマトンの項集合

Page 79: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 79

"read"S1

S7

S8

S2

S3

S4

S5

S6

S10

S11

S12

S14

S15

"print"

Ident

Stat

StatList

{

{

"if"

"print"

"read"

Ident

Stat

(

S13

S16

S17 S18

S19

S20 S21

S22 S23

S24 S25

S26 S27 S28

S9

Ident

Expr

Cond

<

>

Expr

Expr

)

"if"

Expr;

Ident

Ident

Ident

;Ident

=

Ident

Expr ;

{

Ident

StatList

StatList

}

{

Stat

Stat

図 4.8: LR(0)オートマトン

む.これは「S1で Statが読めた」という状況に対応している.そこで S1から Stat

のラベルがついた遷移を行い,S8へ来る.

もしこの"read"が"print"だと,今度は S1から S4,S13と進み,ここで「行き止

り」になり,Identが Exprに還元されると同時に S4へ戻ってここから改めて S22へ

進み,ここでようやく";"がシフトされて S23へ進み,print文全体が還元されて S1

へ戻る.このように,「シフトすべきか,還元すべきか」という選択はオートマトンの

状態を通じて的確に指示される.

4.3.4 SLR(1)解析器

先の説明では「行き止り」になったとき,•が最後にある項 (「行き止り」だから必ず

そういう規則がある)を用いて還元していた.だが,行き止りではないが •が最後にも

つ項も含んでいる状態もあり得るし,還元に使える項が複数ある可能性もある.これ

らの場合の動作はオートマトンだけでは決まらず,別の情報を用いなければならない.

Page 80: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

80 第 4章 構 文 解 析

1つの考え方として,選択を Follow集合に基づいて行うという方針があげられる.つ

まり,ある状態に [ A → α• ]なる項が含まれ,かつ次の先読み記号 tが Follow(A)

に含まれているときのみこの項に対応する規則による還元を行う.入力が構文的に正

しいならばAの後に来る端記号は Follow(A)に含まれるはずなので,もしそうでない

なら還元してはならないのは明らかである.この方針に基づく解析器を SLR(1)解析

器とよぶ (最初の Sは Simlpeの意).

SLR(1)解析器を構成するために,オートマトンの各状態の red属性 (前に準備ずみ)

に,「この先読み記号が来たらこの規則による還元」という情報を連想リストとして保

持する.SLR(1)解析器のための還元情報を計算する関数 compslr1は次の通り.

(defun compslr1 (&aux s0 i x)

(for s0 *state-list* ; (1)

(for i (s-items s0)

(if (null (i-post i)) ; (2)

(for x (s-follow (i-lhs i)) ; (3)

(push (list x (i-rule i)) (s-red s0))))))) ; (4)

(1)各状態について (2)その状態に含まれる各項のうち •が末尾にあるものを見つけ,

(3)その項に対応する生成規則の左辺の Follow に含まれる各記号については,(4)そ

の記号が来たら対応する生成規則により還元する,という情報を属性 redにつけ加え

る.これの実行結果を図 4.9に示す.図 4.1の文法では先読み記号によって還元規則が

違うことはなく,結果的に「行き止りに来たら還元」に近い動作になるが,ただし先

読み記号が左辺の Followになければ還元しても無駄なのでただちにエラーとなる点

は違っている.

LL(1)のときと同様,以上のようにして生成された情報によって全ての解析動作が

1通りに定まる場合に,その文法は SLR(1)であると言う.そうでない場合とは,属性

gotoと redに同じ先読み記号が現れる場合,および redが同じ先読み記号に対して複

数の規則を指示する場合 (およびそれらの複合)である.前者の現象をシフト-還元衝突

(shift-reduce conflict),後者を還元-還元衝突 (reduce-reduce conflict)

とよぶ (衝突についてはあとでもう 1度述べる).

解析動作が 1通りに定まるものとして,以上のようにして準備した情報を元に構文

解析を実行する関数 lrparseを次に示す.

(defun lrparse (l &aux stack s x y)

Page 81: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 81

>(compslr1)

NIL

>(for s *state-list* (format t "~% ~A ~A" s (s-red s)))

S1 ((EOF R3) (} R3))

S2 NIL

S3 NIL

S4 NIL

S5 NIL

S6 ((EOF R3) (} R3))

S7 ((EOF R1))

S8 ((EOF R3) (} R3))

S9 ((EOF R2) (} R2))

S10 NIL

S11 ((IDENT R8) (READ R8) (PRINT R8) (IF R8) ({ R8) (EOF R8) (} R8))

S12 NIL

S13 ((SEMI R11) (< R11) (RPAR R11) (> R11))

S14 NIL

S15 NIL

S16 NIL

S17 NIL

S18 ((RPAR R10))

S19 ((RPAR R9))

S20 NIL

S21 ((IDENT R7) (READ R7) (PRINT R7) (IF R7) ({ R7) (EOF R7) (} R7))

S22 NIL

S23 ((IDENT R6) (READ R6) (PRINT R6) (IF R6) ({ R6) (EOF R6) (} R6))

S24 NIL

S25 ((IDENT R5) (READ R5) (PRINT R5) (IF R5) ({ R5) (EOF R5) (} R5))

S26 NIL

S27 NIL

S28 ((IDENT R4) (READ R4) (PRINT R4) (IF R4) ({ R4) (EOF R4) (} R4))

NIL

図 4.9: SLR(1)構文解析器の還元情報

Page 82: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

82 第 4章 構 文 解 析

(setq stack (list (car *state-list*)))

(setq l (append l (list ’eof))) ; (1)

(loop

(setq x (car l)) (setq s (car stack)) ; (2)

(format t "~% ~A : ~A" (reverse stack) x)

(cond ((setq y (assoc x (s-goto s))) ; (3)

(push x stack) (push (cadr y) stack) (pop l))

((setq y (assoc x (s-red s))) ; (4)

(setq r (cadr y))

(format t "~% ~A: ~A -> ~A" r (r-rhs r) (r-lhs r))

(if (eq r ’r1) (return)) ; (5)

(for z (r-rhs r) (pop stack) (pop stack)) ; (6)

(setq s (car stack)) (setq y (assoc (r-lhs r) (s-goto s)))

(push (r-lhs r) stack) (push (cadr y) stack)) ; (7)

(t ; (8)

(format t "~% syntax error.") (return)))))

(1)解析スタックには初期状態のみ積み,入力列の末尾に eofをつけ加えた状態から始

める.(2)ループに入って,まずスタック先頭の状態と先読み記号を参照する.(3)こ

の状態で先読み記号に対する gotoが指定されていれば,先読み記号と行き先状態を順

に解析スタックに積み,次のループ周回に進む.(4)そうでなく,この状態で先読み記

号に対する還元が指定されていれば,(5)まず規則を見てそれが R1だったら出発記号

まで来たので終る.(6) そうでなければ規則の右辺の記号数の倍,スタックを取り降ろ

した後,再度スタックの先頭状態を見て,還元した規則の左辺 (非端記号)により再度

gotoを検索し,(7)左辺の記号と検索した状態をスタックに積む.(8)行き先が見つか

らなかった場合構文エラーとする.この関数での解析の実行例を次に示す.

>(lrparse ’(if lpar ident < ident rpar print ident semi))

(S1) : IF

(S1 IF S5) : LPAR

(S1 IF S5 LPAR S12) : IDENT

(S1 IF S5 LPAR S12 IDENT S13) : <

R11: (IDENT) -> EXPR

(S1 IF S5 LPAR S12 EXPR S15) : <

(S1 IF S5 LPAR S12 EXPR S15 < S16) : IDENT

(S1 IF S5 LPAR S12 EXPR S15 < S16 IDENT S13) : RPAR

R11: (IDENT) -> EXPR

(S1 IF S5 LPAR S12 EXPR S15 < S16 EXPR S19) : RPAR

R9: (EXPR < EXPR) -> COND

Page 83: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 83

(S1 IF S5 LPAR S12 COND S14) : RPAR

(S1 IF S5 LPAR S12 COND S14 RPAR S20) : PRINT

(S1 IF S5 LPAR S12 COND S14 RPAR S20 PRINT S4) : IDENT

(S1 IF S5 LPAR S12 COND S14 RPAR S20 PRINT S4 IDENT S13) : SEMI

R11: (IDENT) -> EXPR

(S1 IF S5 LPAR S12 COND S14 RPAR S20 PRINT S4 EXPR S22) : SEMI

(S1 IF S5 LPAR S12 COND S14 RPAR S20 PRINT S4 EXPR S22 SEMI S23) : EOF

R6: (PRINT EXPR SEMI) -> STAT

(S1 IF S5 LPAR S12 COND S14 RPAR S20 STAT S21) : EOF

R7: (IF LPAR COND RPAR STAT) -> STAT

(S1 STAT S8) : EOF

R3: NIL -> STATLIST

(S1 STAT S8 STATLIST S9) : EOF

R2: (STAT STATLIST) -> STATLIST

(S1 STATLIST S7) : EOF

R1: (STATLIST) -> PROGRAM

確かに図 4.4と同じようにシフトと還元が起こっている.なお,ここではわかりや

すさのためスタック上に構文記号と状態を交互に積んでいたが,関数 lrparseを見る

とスタックに積んだ構文記号は参照されていない.その理由は,オートマトンの状態

には「現在どのような入力列を読んだところか」という情報が必要なだけ含まれてい

るためである.したがって,実際には解析スタック上には状態だけ積めば十分である.

ここでは行き先の情報と還元の情報を分けていたが,一般には状態と先読み記号の

対から「シフトしてどの状態へ行く」か「どの規則で還元する」のいずれかを指示す

る動作表 (action table)と,状態と非端記号の対から「還元した後どの状態に進むか」

を指示する行き先表 (goto table)の組で表すことが多い.この 2つの表を合せて構文

解析表 (parsing table)とよぶ.先の例を構文解析表の形に書いたものを図 4.10に示す

(「規則 R1での還元」は「解析の成功」に置き換えてある).

4.3.5 正準LR(1)解析器

SLR(1)構文解析では還元の選択を Follow集合によっているが,Followはもともと

構文全体を通してどこかで後続記号になっているかを見るものなので,細かい文脈の

区別には不向きである.例えば図 4.11の文法を考えてみる.これは Cのように「式は

単独でも文になり得,また代入の左辺はポインタ参照でもよい」ような言語の文法を

Page 84: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

84 第 4章 構 文 解 析

S1S2S3S4S5S6S7S8S9S10S11S12S13S14S15S16S17S18S19S20S21S22S23S24S25S26S27S28

ident = ; ( ) { } < > if read print stlist stat cond expr

87s6 s5 s3 s4s2

eof

r3r3s26

s2422s13

s12r3 r3 89s6 s5 s4s3s2

accept89s6 s5 s4s3s2 r3 r3

r2r2s11

r8 r8 r8r8r8r8r81514s13

r11 r11 r11r11s20

s16 s1718s1318s13

r10r9

21s6 s5 s4s3s2r7 r7 r7r7r7r7r7

r6 r6 r6 r6 r6 r6 r6

r5 r5 r5 r5 r5 r5 r5

r4 r4 r4 r4 r4 r4 r4

s23

s25

27s13s28

行先表動作表

図 4.10: 図 4.1の言語に対応する SLR(1)構文解析表

Page 85: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 85

program = stat

stat = left "=" right | right

left = ident | "*" right

right = left

図 4.11: ポインタ代入をもつ簡単な文法

>(for s *state-list* (format t "~% ~A ~A ~A" s (s-goto s) (s-red s)))

S1 ((LEFT S6) (RIGHT S5) (STAT S4) (IDENT S3) (* S2)) NIL

S2 ((LEFT S9) (RIGHT S10) (IDENT S3) (* S2)) NIL

S3 NIL ((= R4) (EOF R4))

S4 NIL ((EOF R1))

S5 NIL ((EOF R3))

S6 ((= S7)) ((EOF R6) (= R6))

S7 ((LEFT S9) (RIGHT S8) (IDENT S3) (* S2)) NIL

S8 NIL ((EOF R2))

S9 NIL ((EOF R6) (= R6))

S10 NIL ((= R5) (EOF R5))

NIL

図 4.12: SLR(1)による解析情報

簡略化したものになっている.これから先のプログラムにより SLR(1)構文解析器を

つくろうとすると図 4.12のようになる.これを見ると,初期状態 S1から Leftにより

遷移した後の状態 S6で,次の記号が=のときシフトするか Leftを Rightに還元する

かで衝突がある.これは,Rightが=の前に来ることが可能なため Follow(Right)に

=が含まれているからである.しかし実際には代入記号の直前にいるのだから Leftを

Rightに還元するのは誤りである.このような Followによる「おおざっぱな判定」の

弱点を克服するため,最初から状態の中に先読み記号も含めて計算を行うことを考え

る.そこで,今度は項の中に

[ Program → • Stat ; EOF ]

[ Right → Left • ; "=" ]

のように先読み記号を含めることにする.このような項を LR(1)項とよぶ (1という

のは先読み記号の長さを示す).先読み記号は各項ごとに laという属性に保持するこ

とにして,それをアクセスするマクロを追加する.

Page 86: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

86 第 4章 構 文 解 析

(defmacro i-la (x) ‘(get ,x ’la))

newitemも先読み記号を含めるように直す.

(defun newitem1 (r pre post la &aux x)

(setq x (newsym "I" (incf *item-count*))) (push x *allitem*)

(setf (i-rule x) r) (setf (i-pre x) pre) (setf (i-post x) post)

(setf (i-la x) la) (setf (i-lhs x) (r-lhs r))

(setf (i-next x) (if post (newitem1 r (cons (car post) pre)

(cdr post) la)))

(setf (i-str x) (format nil "[~A ~:A ~:A; ~A]"

(i-lhs x) (reverse pre) post la))

x)

また inititemmsも各規則の各 •位置ごとに加えて,各端記号ごとに 1個の項をつ

くることになる.

(defun inititems1 ()

(setf (s-term ’eof) t) (setf (s-first ’eof) ’(eof))

(setq *item-count* 0) (setq *allitem* nil)

(for s (cons ’eof *terms*)

(for r *rule-list* (newitem1 r nil (r-rhs r) s)))

(setq *allitem* (reverse *allitem*)))

eofは文法の上では端記号ではないが,以下では端記号の 1つであるかのように扱い,

firstなどの属性ももたせる.閉包は「•の直後の記号を左辺にもち,なおかつそれよ

りさらに左側の全記号列の末尾にこの項の先読み記号を付けたもの全体の Firstのど

れかに一致する先読み記号をもつものを集めてくる」ように直す (*の箇所).

(defun closure1 (l &aux c i)

(while l

(setq i (pop l))

(push i c)

(if (i-post i)

(for j (collect1 (car (i-post i)) ; *

(seqfirst (append (cdr (i-post i)) ; *

(list (i-la i))))) ; *

(if (and (not (member j c)) (not (member j l)))

(push j l)))))

(sortsym c))

Page 87: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 87

(defun collect1 (s l &aux o)

(for i *allitem*

(if (and (eq s (i-lhs i)) (null (i-pre i)) (member (i-la i) l)) ; *

(push i o)))

o)

compgotoと compstatesはここまでに直した関数を呼ぶだけであとは同じでよい.そ

して最後に還元の判断は項に含まれている先読み記号を参照して決める.

(defun complr1 (&aux s0 i x)

(for s0 *state-list*

(for i (s-items s0)

(if (null (i-post i))

(push (list (i-la i) (i-rule i)) (s-red s0)))))) ; *

このようにしてつくった解析表を正準 (cannonical)LR(1)構文解析表,それに従って

動作する解析器を正準 LR(1)解析器とよぶ.先に SLR(1)ではうまくいかなかった文

法の解析情報はこれによると図 4.13のようになる.今度は確かに Leftの後で"="が現

れたときには Rightへの還元は行わないようになっている.このように,正準 LR(1)

は SLR(1)よりも広い文法クラスを含んでいる.実は,正準 LR(1)文法は左から右へ

後戻りなしで解析可能な最大の文法クラスとなっていることが知られている.一方,正

準 LR(1)解析器の問題点はその状態数が非常に多くなるということである.それは文

法における端記号の数が nとして,項の数が SLR(1)の n倍になっていることに起因

する.このため,正準 LR(1)解析器をそのままコンパイラに採用するのは実用的でな

いとされている.

4.3.6 LALR(1)解析器

SLR(1)よりは受け入れられる文法の範囲が広く,しかも LR(1)ほどには状態数が多く

ない実用的な構文解析器として,LALR(1)解析器があげられる.その原理を示すため,

前節の例において生成されたLR(1)各状態に含まれる項集合を図4.14に示す.ここで,

LR(1)項の先読み記号部分 (;のあと)を取り除いてしまったあと集合として整理する

と,S2と S8,S3と S9,S11と S14,S12と S13はそれぞれ同じものになる.そこで,

これらの状態は同一のものとみなして併合する.このときもともとのオートマトンに

Page 88: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

88 第 4章 構 文 解 析

S1 ((LEFT S6) (RIGHT S5) (STAT S4) (IDENT S3) (* S2)) NIL

S2 ((LEFT S14) (RIGHT S13) (IDENT S3) (* S2)) NIL

S3 NIL ((= R4) (EOF R4))

S4 NIL ((EOF R1))

S5 NIL ((EOF R3))

S6 ((= S7)) ((EOF R6))

S7 ((LEFT S11) (RIGHT S10) (IDENT S9) (* S8)) NIL

S8 ((LEFT S11) (RIGHT S12) (IDENT S9) (* S8)) NIL

S9 NIL ((EOF R4))

S10 NIL ((EOF R2))

S11 NIL ((EOF R6))

S12 NIL ((EOF R5))

S13 NIL ((= R5) (EOF R5))

S14 NIL ((= R6) (EOF R6))

図 4.13: 正準 LR(1)による解析情報

おける遷移が正しく併合したオートマトンに移せるかどうかかが問題になるが,実は

LR(1)オートマトンにおける遷移の計算 (compgoto1)は先読み記号には依存していな

い.したがって,状態 AとBが併合されるとして,AからA′ に記号X による遷移が

あれば,B から B′へも記号X による遷移があり,そして B′は実はA′ともとから同

じ状態であるか,あるいは併合される状態である.

このようにしてできたオートマトンの状態数はもとの 14から 4減って 10 であるが,

これは先に同じ文法に基づくLR(0)オートマトン (図 4.8の状態数と同じである.これ

は偶然ではなく,併合によってできたオートマトンの状態数は常にLR(0)オートマト

ンの状態数に等しい 2.しかし,状態数は同一であっても,併合によってできたオート

マトンの方はもともとの LR(1)項にあった先読み記号の情報が利用できる.したがっ

て,正準 LR(1)解析器の場合と同様にして先読み記号を利用して還元先を決めること

ができ,これによって生成される解析器を LALR(1)解析器とよぶ.図 4.11の文法に

対する LALR(1)解析器の解析情報を図 4.15に示す.ところで,LALR(1)は LR(1)よ

り文法クラスが小さいと記した.これは,状態を併合する際,同じ先読み記号に対し

2これは直観的には次のように理解できる.すなわち,はじめから先読み記号を考えないで計算しても,先読み記号まで考えて計算した後で先読み記号の情報を無視して整理しても,計算している対象はあくまでも「出発状態の LR(0) 項集合から始めて,あらゆる構文記号によってどのようなLR(0) 項集合に到達できるか」という同一の情報であるからということである.

Page 89: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 89

S1:[PROGRAM () (STAT); EOF] S5:[STAT (RIGHT) (); EOF]

[STAT () (LEFT = RIGHT); EOF] S6:[STAT (LEFT) (= RIGHT); EOF]

[STAT () (RIGHT); EOF] [RIGHT (LEFT) (); EOF]

[LEFT () (IDENT); EOF] S7:[STAT (LEFT =) (RIGHT); EOF]

[LEFT () (* RIGHT); EOF] [LEFT () (IDENT); EOF]

[RIGHT () (LEFT); EOF] [LEFT () (* RIGHT); EOF]

[LEFT () (IDENT); =] [RIGHT () (LEFT); EOF]

[LEFT () (* RIGHT); =] S8:[LEFT () (IDENT); EOF]

S2:[LEFT () (IDENT); EOF] [LEFT () (* RIGHT); EOF]

[LEFT () (* RIGHT); EOF] [LEFT (*) (RIGHT); EOF]

[LEFT (*) (RIGHT); EOF] [RIGHT () (LEFT); EOF]

[RIGHT () (LEFT); EOF] S9:[LEFT (IDENT) (); EOF]

[LEFT () (IDENT); =] S10:[STAT (LEFT = RIGHT) (); EOF]

[LEFT () (* RIGHT); =] S11:[RIGHT (LEFT) (); EOF]

[LEFT (*) (RIGHT); =] S12:[LEFT (* RIGHT) (); EOF]

[RIGHT () (LEFT); =] S13:[LEFT (* RIGHT) (); EOF]

S3:[LEFT (IDENT) (); EOF] [LEFT (* RIGHT) (); =]

[LEFT (IDENT) (); =] S14:[RIGHT (LEFT) (); EOF]

S4:[PROGRAM (STAT) (); EOF] [RIGHT (LEFT) (); =]

図 4.14: LR(1)オートマトンの状態集合

Page 90: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

90 第 4章 構 文 解 析

S1 ((LEFT S6) (RIGHT S5) (STAT S4) (IDENT S3+S9) (* S2+S8)) NIL

S2+S8 ((LEFT S11+S14) (RIGHT S12+S13) (IDENT S3+S9) (* S2+S8)) NIL

S3+S9 NIL ((= R4) (EOF R4))

S4 NIL ((EOF R1))

S5 NIL ((EOF R3))

S6 ((= S7)) ((EOF R6))

S7 ((LEFT S11+S14) (RIGHT S10) (IDENT S3+S9) (* S2+S8)) NIL

S10 NIL ((EOF R2))

S11+S14 NIL ((= R6) (EOF R6))

S12+S13 NIL ((= R5) (EOF R5))

図 4.15: LRLA(1)の構文解析情報

て異なる還元動作を行う状態どうしが併合されてしまい,還元-還元衝突が起きて動作

が一意的に決まらなくなる場合が存在し得るためである.そのような場合にはその文

法は LALR(1)ではない 3.

なお,解析表の生成時だけとは言え,正準 LR(1)オートマトンをいったんつくるの

では計算の手間が大きいが,LR(1)オートマトンを経ずに直接必要な状態のみを生成

しながら合せて先読み記号の情報を計算する手順が知られている (練習問題参照).

4.3.7 再帰上昇解析器

ここまで上向き解析はシフト-還元解析器によることを前提としてきたが,スタックを

用いた LL(1)解析器に対応して再帰下降解析器があるのと同様に,シフト-還元解析器

と同等の動作を一群の互いに呼び合う手続き群で実現することができる.これを再帰上

昇解析器 (recursive ascent parser)とよぶ.再帰上昇解析器においては,まず LRオー

トマトンの各状態を 1つの手続きに対応させる.そして,LR解析器の動作を 1つの状

態に限って考えると次のような動作が必要であることがわかる.

1. 最初にある状態に来たときは,入力記号を参照してシフトか還元かを決める.

2. シフトの場合は,入力記号に応じた後続状態に移るが,やがて還元が起きて戻っ

てくる.そのときこの状態に対して起きることは次のいずれかである.3一方,併合によってシフト-還元衝突が新たに生み出されることはない.というのは,併合後の状態に

記号 X による遷移があれば,併合する前の各状態も X による遷移をもっていたはずだから.

Page 91: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.3. 上 向 き 解 析 91

2a. 規則の右辺の数だけ状態を取り降ろす際にスタックから取り降ろされてな

くなる.これは,この状態よりさらに前の状態への戻りを意味する.

2b. 還元の結果この状態に戻ってきて,規則の左辺に基づき次の状態へ移る.

3. 還元の場合は,対応する還元動作を行う.すなわち,生成規則 1の還元なら解析

終了だが,そうでなければ右辺の数 nだけ状態を取り降ろす.これはすなわち,

この状態へやってきた n個前の状態への戻りを意味する (2aと対応).

なお,1と 3は各状態に到達した後 1回しか起きないが,2が選ばれた場合には 2bは

最後に 2aによって戻るまで何回も繰り返し起きる.

上の動作を Lispコードで実現することを考える.まず,入力列は広域変数*i*に,ま

たシフトおよび還元の際次の状態の検索に使う記号は広域変数*x*に保持する.さら

に,上記 2aの n段の戻りに対応するため還元時には右辺の長さを広域変数*l*に入れ

るものとする.そして,各状態に対応する手続きのコードは次のような形になる.

(defun Sxx ()

(case (car *i*)    ; (1)

  ((還元すべき記号…)

     … (setq *x* ’左辺) (setq *l* 長さ) (return-from Sxx) ; (2)

...    ; (2)

  ((シフトすべき記号…) (setq *x* (pop *i*))      ; (3)

(t (throw ’res ’error))) ; (4)

(loop (case *x* ; (5)

    ((記号…) (Syy)) ; (6)

...) ; (6)

(if (> (decf *l*) 0) (return-from Sxx))))) ; (7)

(1)まず入力記号の先頭を見て,(2)この状態で還元すべき記号のどれかであればそれ

に応じた規則による還元の処理を行い,*x*に左辺の記号,*l*に右辺の長さを入れて

この状態から戻る.(3)シフトすべき記号であれば,入力列を読み進め,その記号を

*x*に入れておく.(4)それ以外の記号であれば構文誤りである.次に (5)ループに入

り,*x*の内容に応じて (6)次の状態に対応する手続きを呼ぶ.(7)戻ってきたとき*l*

の値が 1より大きいならこの状態は取り降ろされるので対応して戻るが,その際*l*は

1減らしておく.

以上のやり方で,図 4.11の文法に対する図 4.15と同等に動作する再帰上昇解析器を

図 4.16に示す.ただし上の枠組通りだとかなり長くなるのでシフトのみ,還元のみ等

Page 92: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

92 第 4章 構 文 解 析

図 4.16: 再帰上昇解析器の例

の場合には適宜記述を短くしてある.これをトレースつきで実行させた様子を示して

おく.

>(raparse ’(* ident = ident))

1> (S1)

SHIFT: *

2> (S2S8)

SHIFT: IDENT

3> (S3S9)

R4: ident -> left : (= IDENT EOF)

<3 (S3S9 1)

3> (S11S14)

R6: left -> right : (= IDENT EOF)

<3 (S11S14 1)

3> (S12S13)

R5: * right -> left : (= IDENT EOF)

<3 (S12S13 2)

<2 (S2S8 NIL)

2> (S6)

SHIFT: =

3> (S7)

SHIFT: IDENT

4> (S3S9)

R4: ident -> left : (EOF)

<4 (S3S9 1)

4> (S11S14)

R6: left -> right : (EOF)

<4 (S11S14 1)

4> (S10)

R2: left = right -> stat : (EOF)

<4 (S10 3)

<3 (S7 NIL)

<2 (S6 NIL)

2> (S4)

R1: stat -> program : (EOF)

OK

確かに,これまでのシフト-還元解析器の状態遷移と同等の手続き呼出し系列となる

Page 93: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.4. 実用のための構文解析 93

のがわかる.再帰上昇解析器は各手続きがオートマトンの状態であるため再帰下降解

析器ほどわかりやすくはないが,解析の中途で付加的な作業を行ったり余分な先読み

を行う等の手直しが容易だという特徴は同じであり,実行速度も表とドライバを組み

合せたシフト-還元解析器より速いとされている.

4.4 実用のための構文解析

4.4.1 曖 昧 な 文 法

ここまでは曖昧でない文法のみを想定して来たが,場合によっては曖昧な文法を許す

方が人間,計算機双方にとって有利なこともある.その代表的な例としてぶらさがり

else(dangling else)の問題がある.多くの言語では if文に対して

Stat → "if" Cond "then" Stat "else" Stat

Stat → "if" Cond "then" Stat

という構文定義を用いるが,その場合 if C1 then if C2 then A else Bという文は

if C1 then ( if C2 then A else B ) -- (1)

if C1 then ( if C2 then A ) else B -- (2)

の 2通りに解釈でき曖昧である.そこで「ただし,おのおのの else はまだ対応する

elseをもたない最も近い ifに対応させる」という規則を (自然言語で)付すのが恒例

である (これにより上の解釈 (2)は葬り去られる).代りに文法を

BalStat → "if" Cond "then" BalStat "else" Stat | 各種の文Stat → "if" Cond "then" Stat | BalStat

と直しても曖昧さは解消できるのだが,このように変形した文法は見やすくない.

文法が曖昧なまま構文解析器をつくろうとすると,例えば LR構文解析の場合には

それはシフト-還元衝突として現れる.これは,文法の曖昧さに対応して「then部だ

けから成る if文を全て読み終った時点とも,else部まである if文の elseの直前と

も取れる」状態が現れることに起因する.そこで,このような状態では常にシフトす

る—つまり,後者の解釈を取る—ことにして解析表をつくってしまう.これによって

構成される解析器はまさに上述の「elseを最も近い ifに対応させる」ような解析動

作を行う.LL(1)解析器の場合でもこれと同様なことが行える.

Page 94: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

94 第 4章 構 文 解 析

曖昧な文法が役に立つもう 1つの代表例は式と演算子の構文である.「Expr → Expr

"+" Expr」という文法は「1 + 2 + 3」を「(1 + 2) + 3」とも「1 + (2 + 3)」と

も解釈できる.この曖昧さを除くには,式-項-因子のように演算子の強さごとに別の非

端記号を使用すればよいが,これも読みやすくない.曖昧な文法のまま LR構文解析

表をつくろうとすると,「1 + 2と 2 + 3 のどちらを先に還元するか」でシフト-還元

衝突が起きる.ここで+は左結合的 (left-associative)だから,前者を選択し,還元を行

うように解析表をつくればよい.

さらに,演算子間の順位や,右結合的や (right-associative,ベキ乗が通常そうであ

る),無結合的 (non-associative,多くの言語では比較演算は「a = b = c」とは書けな

いのでそうである)な演算子も同様に扱える.例えば次の例を見てみる (<>は比較で無

結合的,+と*は左結合的,^は右結合的で強さはあとのものほど強いとする).

(setq *nonterms* ’(program expr))

(setq *terms* ’(ident = <> + * ^))

(setq *rules*

‘((program ident = expr) (expr expr <> expr)

(expr expr + expr) (expr expr * expr) (expr expr ^ expr) (expr ident)))

これをもとに SLR(1)による解析情報を計算すると次のようになる.

S1 ((IDENT S2)) NIL

S2 ((= S3)) NIL

S3 ((EXPR S5) (IDENT S4)) NIL

S4 NIL ((EOF R6) (<> R6) (+ R6) (* R6) (^ R6))

S5 ((^ S9) (* S8) (+ S7) (<> S6)) ((EOF R1))

S6 ((EXPR S13) (IDENT S4)) NIL

S7 ((EXPR S12) (IDENT S4)) NIL

S8 ((EXPR S11) (IDENT S4)) NIL

S9 ((EXPR S10) (IDENT S4)) NIL

S10 ((^ S9) (* S8) (+ S7) (<> S6)) ((EOF R5) (<> R5) (+ R5) (* R5) (^ R5))

S11 ((^ S9) (* S8) (+ S7) (<> S6)) ((EOF R4) (<> R4) (+ R4) (* R4) (^ R4))

S12 ((^ S9) (* S8) (+ S7) (<> S6)) ((EOF R3) (<> R3) (+ R3) (* R3) (^ R3))

S13 ((^ S9) (* S8) (+ S7) (<> S6)) ((EOF R2) (<> R2) (+ R2) (* R2) (^ R2))

ここで S10~S13がシフト-還元衝突の宝庫だが,これはいずれも上述の曖昧さに由

来する.ここで例えば S5で^を見ると S9→ S10へ来るので,S10は左に^を見た状態

だとわかる.したがって,次の記号が^のときのみシフト,あとは (順位が一番高いの

Page 95: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.4. 実用のための構文解析 95

だから)全て還元を選ぶべきである.S11は左に*なので,先読み記号が^のときシフト,

あとは還元.S12 は左に+なので,先読み記号が^と*のときシフト,あとは還元.S13

は左に<>なので,^*+のどれでもシフト,最後に eofは還元だが,<>は構文エラーに

なるのでシフトも還元も取り除くべきである.

このように,曖昧な文法を使用することの利点は 1つは構文記号の数が少なく人間

にとってわかりやすくなることであるが,同時に解析器の側でも状態数が少なくてす

み,また余分な還元がなくなるので効率よく解析が行えるという利点がある.

4.4.2 誤りからの回復

現実の解析器が直面する問題の 1つは,ソースプログラムが必ずしも構文的に正しい

ものだけとは限らない,という点である.ここまでに扱った解析手法ではいずれも,構

文誤りがあった場合にはそこから先へは解析を進めないようになっていた.これをその

ままコンパイラに適用したとすれば,最初の構文誤りがあった時点でエラーメッセー

ジを出して止まってしまうことになる.

もちろんこれは,コンパイラの動作としては望ましくない.構文誤りがあった場合で

も,できるだけ先までソースプログラムを解析し,別の構文誤りや意味的誤りをなる

べく多く発見することが望ましい.そのためには何らかの方法でエラー状態から脱し,

構文解析を続行する必要がある.このような処理を誤り回復 (error recovery)とよぶ.

回復の最も基本的なやり方は,パニックモード (panic mode)とよばれるものである.

例えば,多くのプログラミング言語では「文」のような手頃な単位があり,しかも文

と文の境目は「;」「if」などの特徴的な記号によってそれと知ることができる.そこ

で,文の内部で構文誤りに直面したら「パニックモード」に入り,「文の並び」など手

頃なレベルまで戻るとともに入力も文の境目と思われるところまで読み捨てる.その

後はパニックモードから復帰して,次の文から解析を続行すればよい.この場合,誤

りの場所から次の文までの間は読み捨てられ検査されないが,通常の言語では 1つの

文がそれほど大きくないのでこれで十分である.なお,「文の並びまで戻る」という操

作は再帰下降解析であれば文の並びに対応する関数まで次々に呼出しから戻ることに,

また LL(1)や LR解析器では解析スタックの先頭に「文の並び」に対応する記号や状

態が現れるまでスタックを取り降ろすことに対応する.

Page 96: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

96 第 4章 構 文 解 析

これをもう少し定式化するにはエラー記号を用いる.この場合は,次のように構文

記述の中にあらかじめエラー記号とよばれる特殊な記号を含めておく.

Stat → Error ";"

そして,構文誤りに遭遇したとき,解析プログラムはできるだけ少ない量のスタック

内の状態群と入力列をこのエラー記号に還元することで解析を継続しようとする.上

の例の場合は,結果として文への還元が許される状態までスタックを取り降ろし,入

力を次の;まで読み進めた後全体を「誤りのあった文」として還元するので,起こるこ

とは上述のパニックモードに類似している.ただし,この方法では,式,引数などよ

り小さいレベルまでエラー記号を挿入することで読み捨てられる入力を小さくできる.

1つ問題なのは,右辺がエラー記号のみから成る規則をつくり,空列からエラー記

号への還元を許した場合,エラー記号への還元が連続して起こるだけで入力が先へ進

まなくなる可能性があることである.これに対処するため,yaccなどのツールではエ

ラー記号への還元後に決まった個数以上入力記号をシフトする前に再度エラーになっ

たら,最初の回復が失敗したとみなし,元へ戻って入力記号を余分に読み捨てて再度

やり直すようになっている.

誤りから回復するより積極的な方策は,構文誤りを含んだソースプログラムが「本

来はどのようなプログラムであるはずたったのか」を類推し,そのように入力列を修

正して解析を続行することである.このやり方を誤り修復 (error correction)とよぶ.

誤り修復では入力の読み捨てではなく,欠けていると思われる端記号を挿入して修復

を行うこともできる.特に挿入のみによってエラー修復を行う場合にはソースコード

から受け取った情報を 1 個も読み捨てることなく誤り回復を行える.

最後に,エラー回復/修復に対する価値感は使用される計算機環境によって変化する

ことを注意しておく.例えば 1日に数回しか翻訳が行えない環境であれば軽微な誤り

をうまく修復してもらえることはプログラマにとって価値がある.しかし,繰り返し

翻訳することが苦痛でない環境であれば,修復に時間を費やすよりはさっさと誤り箇

所を教えてくれる方が有難いかもしれない.修復アルゴリズムが真にプログラマの意

図通りにソースプログラムを修復できる割合いが高くないなら特にそうである (例えば

begin/endの入れ間違いなどを正しく修復することは本質的に不可能である).

Page 97: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.5. 構文解析器生成系 97

4.4.3 解析表の圧縮

LL(1)解析器や LR解析器は,構文をもとにあらかじめ用意した解析表とそれを参照

しながら解析を実行するドライバに分かれている.ドライバ自体はさほど大きいもの

ではないが,解析表の大きさは SLR(1)解析器や LALR(1)解析器でも数十KB~数百

KB程度の領域を必要とすることが多い.

そこで,構文解析表を圧縮して領域を減らすことも多く行われる.例えば図 4.10を

見ると,構文解析表の多くの部分はエラーエントリ (空白で表されている)である.そ

こで,空白でない所のみの情報を詰めて保持し,参照時に検索を行うなどの手段をと

ることができる.また,これらの一般的手法に加えて,構文解析表に固有の特徴を活

かした圧縮技法がいくつか知られている.その 1つは,構文解析表では複数の行が全

く同じ内容である場合が多いことを利用するものである.例えば図 4.10の解析表を見

ると S6と S8,S16と S17の行はそれぞれ同一である.そこで,動作表や行先表の本

体は分けて格納し,各状態から表本体への対応表を別にもつことでこれら同一内容の

行を共有できる.その際,S12や S26と S16/S17のように動作表のみ (または行先表

のみ)同一な場合もその部分だけ共有できる.

また,S20と S6/S8を比べると前者は後者から r3のエントリを除いたものである.

この場合にもこれらを共有してよい.この場合には解析器の動作が変化するが,これ

は本来エラーで止まるべき所で余分な還元 (この場合は S20における r3)が起きるだ

けで,その後で先読み記号をシフトできなくなって停止する.したがって,正しくな

い構文を正しいと誤認する恐れはなく,エラーを検出する位置も変化しない.

4.5 構文解析器生成系

解析表とドライバが分かれた形の構文解析器では,コンパイラ作成者が自ら解析表の

計算を行うことはほとんどなく,構文記述を入力すると解析表を作成してくれるツー

ルを使用するのが普通である.これを構文解析器生成系 (parser generator)とよぶ.こ

こではUNIXに標準のツールとして付属し広く使われている構文解析器生成系yaccを

取り上げて例を示す.

図 4.1の構文を少し直して if文に else部をつけ,複数の強さの演算子をもつように

増やしたものの yaccによる記述を次に示す.

Page 98: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

98 第 4章 構 文 解 析

%start program

%token IF ELSE READ PRINT IDENT ICONST

%nonassoc ’>’ ’<’

%left ’+’ ’-’

%left ’*’,’/’

%right ’^’

%%

program : statlist

;

statlist :

| statlist stat

;

stat : IDENT ’=’ expr ’;’

| READ IDENT ’;’

| PRINT expr ’;’

| IF ’(’ expr ’)’ stat

| IF ’(’ expr ’)’ stat ELSE stat

| ’{’ statlist ’}’

;

expr : expr ’<’ expr

| expr ’>’ expr

| expr ’+’ expr

| expr ’-’ expr

| expr ’*’ expr

| expr ’/’ expr

| expr ’^’ expr

| IDENT

| ICONST

;

yaccソースは lexと同様,最初に宣言部を置く.ここでは,まず%startによりこの

構文記述の出発記号を指定している.次に端記号と非端記号を区別する必要があるの

で,%tokenにより端記号として使われる名前を列挙する (これ以外の名前は非端記号

である).また,1文字だけから成る記号は名前の代りに直接「’+’」などのように書

け,自動的に端記号となる.続く%nonassoc,%left,%rightは演算子がそれぞ れ無

結合的/左結合的/右結合的であることを指示する.後の行のものほど演算子の順位は

高くなる.%%の後が構文記述で,各生成規則を

左辺の記号 : 右辺の記号の並び ;

Page 99: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.5. 構文解析器生成系 99

の形で書く.「|」も使うことができる.このソースが yacsam.yaccというファイルに

入れてあるものとして,これを翻訳して解析表を生成するには次のようにする.

% yacc -v yacsam.yacc

conflicts: 1 shift/reduce

文法の曖昧な部分のうち,演算子については順位を指定したので yaccは特に警告せ

ず処理してくれるが,ぶらさがり elseの方は何も指定していないので警告が出る.こ

こで-vオプションを指定したのでオートマトンの情報を y.outputというファイルに

書き出す.これを参照すると

% cat y.output

…37: shift/reduce conflict (shift 38, red’n 7) on ELSE

state 37

stat : IF ( expr ) stat_ (7)

stat : IF ( expr ) stat_ELSE stat

ELSE shift 38

. reduce 7

確かに,シフト-還元衝突は elseの箇所であるとわかる.yaccはシフト-還元衝突があ

る場合にはシフトを優先するので,このままでよい.圧縮された構文解析表とドライ

バルーチンは C言語ソースの形で y.tab.cというファイルに書かれる.これを動かす

ための字句解析部は lexを使うと簡単である.以下に上の yaccソースと組み合せる lex

ソースを示す.

alpha [A-Za-z]

digit [0-9]

%%

if return IF;

else return ELSE;

read return READ;

print return PRINT;

{alpha}({alpha}|{digit})* return IDENT;

{digit}+ return ICONST;

[-+=;(){}<>*/^] return yytext[0];

[\n\t ] ;

. ;

%%

Page 100: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

100 第 4章 構 文 解 析

大文字の IF,ELSEなどは%token宣言の機能の一部として,yaccが生成した y.tab.c

の中で一意的な整数に#defineされる.下から 4行目の記述は,[]内の文字のいずれ

かが現れたらその文字コードそのもの (配列 yytextの先頭に入っている)を返すこと

を意味する.lexの出力は lex.yy.cに生成されるのが,これと先の y.tab.cをまとめ

るのに次の Cソースファイルを用いる.

#include <stdio.h>

main() { yyparse(); }

yyerror(s) char *s; { fprintf(stderr, "%s\n", s); }

#define yywrap() 1

#include "y.tab.c"

#include "lex.yy.c"

すなわち,主プログラムからはただちに構文解析用ドライバを呼び出す.また解析

中にエラーが発見されるとドライバはメッセージ文字列を引数として yyerrorという

名前の関数を呼ぶので,これを用意する.ここでは単に渡されたメッセージを画面に

表示するものとした.また,lexが生成する字句解析器は複数の入力ファイルをまとめ

て翻訳する場合のため,1つの入力ファイルが終りになると yywrapという名前の関数

を呼んで次の入力ファイルへの切換えを試みる.yywrapは入力ファイルがこれ以上な

いときは 1,まだあるときは 0を返すことになっているが,ここでは標準入力のみを

使用するので定数 1に#defineした.その後に y.tab.cと lex.yy.cをこの順で差し

込む (y.tab.cに書かれる #defineを lexソースが参照するので,この順番は守る必

要がある).以上をまとめて実行した結果を示す.

% lex yacsam.lex

% cc yacsam.c

% a.out

if(a > 1) { read a; print a; }

^D%

単に構文解析をするだけなので,出力は何もない.ただし構文が間違っていれば

% a.out

if(a > ) { read a; print a; }

syntax error

%

確かにエラーが出て止まる.演算子の順位や結合がうまく処理されているかどうかも

Page 101: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.6. 練 習 問 題 101

% a.out

if(a + 1 > b - 1) print 1;

if(a > b + 1 > c) print 1;

syntax error

%

正しくない組合せがあると確かにエラーになる.エラーがあると^D(入力終りの印)を

打たなくても解析が終る (エラー回復が行われていない)のに注意.ここで yaccソー

スの statの定義に

| error ’;’

という 1行を追加することで,文の途中でエラーがあったら;まで読み飛ばし次の文

から再開するようにできる 4.このように直した後は

% a.out

if(a > ) print a;

syntax error

b = b +; c = c -;

syntax error

syntax error

エラーがあってもその先の解析を続けるようになる.最後に 2つ構文エラーが出てい

るのは;の前と後のエラーはそれぞれ別に検出できるためである.ただし 1つエラー

があると;まで読み飛ばすので

b = b + - c -;

syntax error

のように 1つの文中の複数のエラーは最初の方しか検出できない.

4.6 練 習 問 題

4-1. 自分が知っている言語 (ないしそのサブセット)の構文を書き記した後,その各

記号の Firstと Followは何になると思うか,直観的に考えて書き出してみよ.

4error はエラー記号として yacc により特別に処理される.

Page 102: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

102 第 4章 構 文 解 析

その後 4.2.1項の手順を手で実行するか Lisp処理系によって実行して Firstと

Followを求め,先の結果と一致しているかどうか確かめよ.直観的に行った場

合どんな所で見落しが起きやすいか.

4-2. 問題 4-1で計算した Frist/Follow集合をもとに,その言語の LL(1)解析器を組

み立てて動かせ.実現に用いる言語は好きなものでよいが,必ず解析表とドライ

バが分離したものにすること.

4-3. 問題 4-2で構成した解析器の解析表を別の言語用のものに取り替えて,確かに別

の言語の解析器ができることを確認せよ.ドライバには手を加えないこと.

4-4. 問題 4-2または 4-3で用いたのと同じ言語の再帰下降型解析器を実現せよ.先に

つくったものとどちらが効率がよいか.どちらがわかりやすいか.

4-5. 図 4.8をよく見ていると,LR(0)オートマトンはもとの構文とどう対応している

かが結構わかるような気がしてくる.そこで,できるだけ (図 4.1よりさらに)簡

単な構文をもつ言語を考案し,その LR(0)オートマトンを直観で描いてみよ.よ

くわからなくなったら,サンプルプログラムをそのオートマトンを用いて構文解

析してみるとよい.

4-6. 4.3.3項に記した手順を手で実行するかまたは Lispにより実行するかまたは yacc

を利用して,問題 4-5と同じ言語のオートマトンを構成してみよ.確かに同じも

のになったか.違ったとすれば,それはどうしてか.

4-7. 構成したオートマトンをもとに,問題 4-5で用いた言語の構文解析表を作成せよ.

また,再帰上昇解析器を構成して動かしてみよ.

4-8. 正準 LR(1)オートマトンから併合により LALR(1)オートマトンをつくる代りに

直接 LALR(1)オートマトンをつくるにはどうしたらよいか考えてみよ.次の 2

つに分けて考えてみるとよい.

• 併合したオートマトンの形は LR(0)オートマトンと同じなのか,違うのか.

違うとしたらそれはどういう所でか.

Page 103: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

4.6. 練 習 問 題 103

• 併合したオートマトンの各状態に含まれる LR(0) 項について,それらを

LR(1)項に戻すための先読み記号を併合前の LR(1)オートマトンからもっ

てくるのではなく,改めて計算して求めるにはどうしたらよいか.

4-9. yaccないしその他の構文解析器生成系が利用可能なら,それを用いて好きな言語

の解析器を生成してみよ.文法をいろいろ書き換えて,その生成系が提供する文

法クラスにどのような文法なら収まるのか試してみよ.

4-10. 問題 4-9で用いた生成系について,オートマトンなどの情報が出力できるなら

それらを観賞せよ.ドライバのソースコードが見えるようなら,解析表をどのよ

うに圧縮しているかも調べてみよ.曖昧な文法やエラー回復の機能が含まれてい

るなら,それをどう実現しているかも考えよ.

Page 104: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス
Page 105: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

105

第5章 意 味 解 析

コンパイラを解析部と生成部に分けた場合,意味解析は解析部の一番最後に位置する.

そして,字句解析には有限オートマトン,構文解析には文脈自由文法という定式化手

法が有効であった.意味解析の場合にはそのような決定的な道具立てはないが,有効

な定式化の枠組もいくらかは存在する.本章では意味解析の機能,および上に述べた

ような範囲で意味解析を定式化する記法とその実装について説明した後,yaccを用い

て構文木のデータ構造を組み立てる例を示す.

5.1 意味解析の役割と位置づけ

コンパイラにおける解析部の最終段階として意味解析部が行うべき典型的な仕事とし

ては次のものがあげられる.

(a) 名前の同定—ソース中に現れたどの名前とどの名前が同じものであり,それぞれ

何を指すか (変数か,型か,ラベルか,など)を定める.

(b) 型情報の付加—ソース中の各変数や式がどのような型をもっているかを定める.

(c) 制御情報の付加—手続きからの戻り文,ループからの脱出文などについて,対応

する手続きやループがどれであるかを定める.

これらは全て,構文定義の枠組では記述されていない制約に対する検査でもある 1.例

えば (a)は変数の 2重定義や未定義の検出,(b)は演算や手続き呼出しにおける型の不

一致検出,(c)は許されない文脈での制御文の使用 (ループ脱出文がループ文に含まれ

1より厳密に言えば,「構文定義の枠組として文脈自由文法を採用した場合には」これらを構文の枠組で記述することができない.文脈依存文法を用いればこれらを扱えることが知られており,また Algol68のように実際にそのような定義を行っている言語もある.しかし,文脈依存文法による記述は読みづらく,またそれに基づく解析手法もあまり研究されていないことはすでに述べた.

Page 106: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

106 第 5章 意 味 解 析

ていないなど)の検出にそれぞれつながる.さらにこれらはそれぞれが互いに関連して

いる.例えば型情報抽出のためには型の名前を正しく同定できなければならず,戻り

文に書かれた式の型が正しいかどうか判定するためにはその戻り文に対応する関数定

義が同定されている必要がある.具体的な名前や型の同定については次章以降で扱う

こととし,以下本章ではこれらの機能をどのように定式化し記述/ 実現するかについ

て取り上げる.

一方,意味解析部は見方によっては一番最初に動く生成部でもある.というのは,解

析部が抽出した情報は何らかの形でコンパイラの後段に渡す必要があるが,そのため

にはその情報を表現するデータ構造を生成する必要があるからである.そこで

(d) 生成 — 後段のための情報を生成する.

も意味解析部の仕事に含まれることになる.その具体的内容はコンパイラの構成によっ

て変化する.例えば,各構文規則が認識されるごとに (つまり構文解析とインタリーブ

して)意味解析を行い,さらにコード生成まで行って目的コードを出力してしまうコン

パイラも多く存在する.このような構成の利点は,同じ情報を何回もたどらずにすむ

ので構造的に簡潔であり,翻訳速度が高めやすい点である.ただし 1パスコンパイラで

は原理的に前方にある情報を参照できないため,コードの品質には多くを望めない.

一方,構文解析の結果をそのまま構文木の形に組み立てた後,この構文木をたどり

ながら意味解析を行う方法もある.この場合,意味解析部の作業は構文解析が全て終っ

たあとで初めて着手される.この方法は情報収集の自由度が高く概念的にもすっきり

しているが,構文木をたどる回数が多くなり,オーバヘッドが大きい.

現実のコンパイラは概ねこの両極端の間であり,構文木を組み立てるという作業と

平行して,木をたどりながら行う処理の 1パス目を同時進行させる.これにより木を

たどる回数を少なくでき,一方で 1パスでは抽出できない情報については後のパスに

任せることができる.

意味解析によって生成される情報の主たる部分は上述のようにソースコードの各動

作文に対応したものであるが,それ以外に上記 (a)~(c)などの情報も何らかの形で後

段に渡される必要がある.そのあり方としてはおおまかに

• 記号表の情報として蓄える.

• 主たる生成物に付随させて蓄える.

Page 107: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.2. 属 性 文 法 107

の 2通りがある.後者の方法はコード生成の際に必要な情報を毎回記号表まで検索し

に行かなくてもアクセスできるという点で便利である.特に構文木に様々な情報を付

随させたものを修飾された (decorated)構文木とよぶ.

5.2 属 性 文 法

意味解析の内容を定式化する手法の代表的なものとして,Knuthが提唱した属性文法

(attribute grammer)がある.属性文法では,各構文記号には 0個以上の属性 (値を蓄

えておけるようなスロット)を付加することができる.例えばプログラムに現れる各式

には (翻訳を前提とした多くの言語では)型が付随しているだろうから,これを Expr

という構文記号に typeという属性をもたせることで表現する.それぞれの属性の値は

Expr.type

のように,構文記号の後に.でつないで属性名を記すことで表す.そして,各属性の計

算規則を構文規則と対にして記す (ここでは構文規則とごちゃまぜにならないように,

計算規則の側を {}で囲むことにする).例えば,次のような属性文法の断片を考える.

Prog = DclList StatList

{ Prog.mesg = DclList.mesg ∪ StatList.mesg

Prog.code = StatList.code

StatList.symtab = DclList.symtab }

上 2つの計算規則はプログラムを翻訳した結果生成されるコードと翻訳時のメッセー

ジを記号 Progのそれぞれ code属性,mesg属性として取り出す.メッセージは宣言部

の翻訳時にも実行文の翻訳時にも生成され得るので,左辺の記号 DclListのmesg属

性と StatListのmesg属性を合せたものとする.また codeの方は宣言部からは生成

されないので,StatListのコードそのままでよい.このように構文木の下側 (つまり

生成規則の右辺)の記号の属性をもとに上側 (つまり左辺)の記号の属性が計算される

場合,その属性を合成属性とよぶ.

3番目の計算規則は DclListを処理した結果の属性 symtab(宣言された名前とその

型の対の集合)を StatListに引き渡す.したがって symtabは StatListにとっては

上から渡される属性になる.このようなものを相続属性とよぶ.この続きをもう少し

示す.

Page 108: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

108 第 5章 意 味 解 析

DclList = nil

{ DclList.mesg = ε

DclList.symtab = φ }DclList = DclList1 "int" Ident ";"

{ x = lookup(DclList1.symtab, Ident.string)

if x = ε then

DclList.symtab = DclList1.symtab ∪ (Ident.string, int)

DclList.mesg = φ

else

DclList.symtab = DclList1.symtab

DclList.mesg = "redefined variable: "||Ident.string

fi }DclList = DclList1 "real" Ident ";"

{ 同様 ... }

宣言が空のときはmesgも symtabも空でよい.次の規則であるが,左辺にも右辺にも

同じ記号 DclListが現れる.このような場合,どちらの記号 (のインスタンス)に付随

した属性かわからないと困るので,適宜添え字をつける.この属性計算の働きは,こ

れまでに宣言された名前の集合中に Identと同じ名前があるかどうか調べ,あれば 2

重宣言のエラーメッセージを発し,なければ新しい名前を集合に追加することである.

ここまでは全て宣言部だったので,実行文の方も少しだけ示す.

StatList = nil

{ StatList.mesg = ε

StatList.code = ε }StatList = StatList1 Ident ":=" Expr ";"

{ Expr.symtab = StatList.symtab

x = lookup(StatList.symtab, Ident.string)

if x = ε then

StatList.mesg = StatList1.mesg ∪ Expr.mesg ∪"variable not declared: "||Ident.string

StatList.code = StatList1.code ∪ error

elseif x.type 6= Expr.type then

StatList.mesg = StatList1.mesg ∪ Expr.mesg ∪"type mismatch in assignment: "||Ident.string

StatList.code = StatList1.code ∪ error

else

StatList.mesg = StatList1.mesg ∪ Expr.mesg

StatList.code = StatList1.code ∪ genassign(x, Expr.code)

fi }

Page 109: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.3. 構文指示翻訳 109

空の並びは前と同様.次に代入文の場合,まず式の中での属性計算にも記号表が必要

だから,symtabを Exprの相続属性として渡す.一方,合成属性であるが,左辺の変

数が宣言されていない場合,および右辺と型が一致しない場合にはmesgにエラーメッ

セージを追加し,codeにも errorという印をはめ込む.誤りがない場合には下請けの

関数に代入文のコードを組み立ててもらってつなぎ込む.symtabを相続属性として渡

す記述と合成属性 (typeなど)の参照が連続しているのは一見奇妙であるが,{}の中の

属性計算は普通のプログラムのように上から下へ実行されるわけではなく,関数型言

語のように単一代入 (各属性値や xのような中間結果は 1回値が決められるとその後は

変化しない)になっていて,値の定義と参照の関係のみに基づいてそれぞれの値が定ま

る.また,計算式における副作用は許されていない.当然,広域変数も存在しない 2.

ここまでの例で属性文法による記述がどのようなものであるかの感じはつかめたこ

とと思うし,属性文法を利用することで意味解析の内容を自然な形で定式化できるこ

とも納得されたことと思う.ただし,意味規則において単一代入による記述を要求し,

副作用を許さないという制約はかなり強い (例えば「コードを出力する手続きを呼ぶ」

こともできない) ので,その理論的な美しさや整合性にも関わらず,実用のコンパイラ

における属性文法の採用例はあまり見られない.

5.3 構文指示翻訳

上記の弱点にも関わらず,「構文規則と意味規則を対にして記述する」という属性文法

のアイデアは意味解析の各部を構文という既知の構成にならって構造化できる点で魅

力的である.そこで,単一代入や副作用の禁止という制約をなくし,意味規則として

一般のプログラミング言語を用いて処理内容を記述する,という枠組が広く使われて

いる.これを構文指示翻訳 (syntax-directed translation)とよぶ.先ほどの例を構文指

示翻訳に直してみる.

Prog = { start(); } DclList StatList { finish(); }

今度は {}の中は例えばC言語等のコードである.start(),finish()は記号表やコー

ド出力ファイルの初期設定と後始末をする手続きなので,属性文法のときと異なり各2このような属性文法の枠組の上で,計算規則全体がどのような参照関係になっている場合に値が一意的

に計算できるか,効率のよい評価順序はどのようなものか,などに関する研究が数多くなされているが,本書では立ち入らない.興味のある読者は参考文献を参照されたい.

Page 110: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

110 第 5章 意 味 解 析

意味規則が「いつ」実行されるかも制御できる必要がある (普通のコードの場合,でた

らめな順序で実行されては役に立たない).そこで,各意味規則は構文規則の右辺の任

意の位置に現れてよく,解析が「その場所まで進んだ時点で」実行されるものとする.

したがって start()はコンパイルの最初に,finish()は最後に実行される.

DclList = nil

DclList = DclList "int" Ident ";"

{ if((x = lookup(Ident.string)) == nil)

enter(Ident.string, TID INT);

else

mesg("redifinition of variable: ", Ident.string); }DclList = DclList1 "real" Ident ";"

{ similar ... }

今度は記号表は内部状態をもっていて副作用として値を登録できる.このため属性文

法のように毎回記号表のインスタンスをもって回ったりする必要はなくなる.メッセー

ジも同様である.これらの結果記述はかなり短くできる.

StatList = nil

StatList = StatList1 Ident ":=" Expr ";"

{ if((x = lookup(Ident.string)) == nil)

mesg("variable undefined: ", Ident.string);

else if(x->type != Expr.type)

mesg("type mismatch in assigment to: ", Ident.string);

else

genassing(x, Expr.dest); }

すなわち,コード (中間コードないし目的コード)もそれぞれの規則が実行されるとと

もにファイルに書き出されていく.ということは,Exprに対応する規則が実行された

時点で式を計算する規則は出力ずみなので,代入に対する規則では式の結果の場所 (た

とえばレジスタ)から変数に値をコピーするコードのみを出す.

このように,構文指示翻訳ではそれぞれの規則が実行される順序やそれに伴う副作

用まで考慮しなければならないが,一方でこれらの事柄を活用して記述を簡潔にでき,

また一般のプログラミング言語のコードを記述するため原理的にはどんなことでも行

わせられる,という柔軟さをもっている.以下では構文指示翻訳が具体的にどのよう

にして実装されるかを,下向き解析と上向き解析の場合で分けて述べる.

Page 111: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.4. 構文指示翻訳の実装 111

5.4 構文指示翻訳の実装

5.4.1 下向き解析器における構文指示翻訳

ここでは下向き解析器として再帰下降解析器を使用する場合を考える.再帰下降解析

器では各非端記号がそれぞれ1つの手続きに対応し,その中には先読み記号に応じてn

個の右辺に対応する分岐があり,各枝は対応する右辺の各記号に応じた呼出しを順に

実行するようになっていた.そこで,各生成規則に意味規則が付加されていたら,呼

出し列中の対応する場所にその意味規則の中味を埋め込めばよい (ただし,各意味規則

は解析器を実現するのと同じ言語で記されているものとする).

次に Expr.typeのような属性の扱いが必要である.属性はプログラミング言語でい

えばレコードのようなものだから,各非端記号ごとに必要となる属性全てを合せもつレ

コード型を定義し,各解析手続きは自分の記号に対応するレコード値を引数としてもつ

ものとする.そして各手続きでは局所変数として右辺に現れる全ての非端記号に対応

するレコードを宣言し,非端記号に対応する手続きを呼ぶときには対応するレコード

を引数として渡すようにする.なお,呼ばれ側から呼び側に属性値を渡す (つまり合成

属性)こともあるので,レコードの受け渡しはそれが許される機構によるべきである.

例として 2進表現の数値を読み込み (10進数値として)打ち出す構文指示翻訳を示す.

Program = BinNum

{ printf("\n%d\n", BinNum.val); }BinNum = nil

{ BinNum.val = 0;

BinNum.len = 0; }BinNum = Digit BinNum1

{ BinNum.val = BinNum1.val +

Digit.val * power(2, BinNum1.len + 1);

BinNum.len = BinNum1.len + 1; }Digit = ’0’

{ Digit.val = 0; }Digit = ’1’

{ Digit.val = 1; }

これを C言語で実現したものを図 5.1に示す.このプログラムは基本的には再帰下降

解析器と同じ構造をもつが,ただし属性を格納するレコードを受け渡している点と各

構文規則の右辺に対応する部分に変換規則が埋め込まれている点が異なる.このよう

Page 112: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

112 第 5章 意 味 解 析

#include <stdio.h>

typedef struct { int val, len; } binnum_attr;

typedef struct { int val; } digit_attr;

static int input;

main() {

input = getchar(); program(); }

program() {

binnum_attr x;

if(input == ’0’ || input == ’1’ || input == EOF) {

binnum(&x); printf("\n%d\n", x.val); }

else {

error(); } }

binnum(a)

binnum_attr *a; {

if(input == EOF) {

a->val = 0; a->len = 0; }

else if(input == ’0’ || input == ’1’) {

digit_attr x; binnum_attr y;

digit(&x); binnum(&y);

a->val = y.val + x.val * power(2, y.len + 1);

a->len = y.len + 1; }

else {

error(); } }

digit(a)

digit_attr *a; {

if(input == ’0’) {

match(’0’); a->val = 0; }

else if(input == ’1’) {

match(’1’); a->val = 1; }

else {

error(); } }

match(c)

int c; {

if(input == c) {

input = getchar(); }

else {

error(); } }

error() {

fprintf(stderr, "syntax error.\n"); exit(1); }

power(n, i)

int n, i; {

int v = 1;

while(--i > 0) v = v * n;

return v; }

図 5.1: 構文指示翻訳の C言語への変換例

Page 113: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.4. 構文指示翻訳の実装 113

に,再帰下降解析器はその構成法が簡単で直感的に把握しやすかったのと同様に,構

文指示翻訳の組込みも直接的でわかりやすく,全体として手作りの構文/意味解析器に

適した手法であるといえる.

なお,この例では変換規則は全て各規則の最後にのみ付加されていたが,常にそれ

だけですむとは限らない.例えば上の例を拡張して,各行に 1個ずつ 2進数が書かれ

ているものを入力としたい場合を考えてみる.一見最初の 2行を次のように変更すれ

ばよさそうに思える.

Program = nil

Program = BinNum ’\n’ Program

{ printf("\n%d\n", BinNum.val); }

しかしこれでは最初に入力した2進数が最後に打ち出されるようになる.なぜなら,こ

れに基づく解析器 (+構文指示翻訳)は次のようになる.

program() {

binnum_attr x;

if(input == ’0’ || input == ’1’ || input == EOF) {

binnum(&x); match(’\n’); program();

printf("\n%d\n", x.val); }

...

最初の 2進数は最初の programの呼出しの最後に打ち出されるが,それが起きるの

は 2番目の 2進数を 2番目の programの呼出しが処理し打ち出した後になるのである.

これを直すには次のように変換規則を右辺の途中に移す.

Program = nil

Program = BinNum ’\n’

{ printf("\n%d\n", BinNum.val); }Program

これに対応するプログラムは次のようになろう.

program() {

binnum_attr x;

if(input == ’0’ || input == ’1’ || input == EOF) {

binnum(&x); match(’\n’);

printf("\n%d\n", x.val);

program(); }

...

Page 114: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

114 第 5章 意 味 解 析

このように,構文指示翻訳では副作用が使える分だけ柔軟であるが,その反面「い

つ」規則が実行されるかを注意深く計画する必要がある.

以上は再帰下降解析器の場合であったが,表を参照しながら動作する下向き解析器

の場合にも表に「各記号はどんな属性をもつか」「各規則の右辺のどの場所に来たらど

の動作を行うか」の情報をもたせることで構文指示翻訳を実装することができる.

5.4.2 上向き解析器における構文指示翻訳

次に上向き解析器 (LR解析器)への構文主導変換の組込みを扱う.まず簡単のため,意

味規則が右辺の最後のみに現れる場合を考える.この場合には意味規則の起動は簡単

で,規則が還元されるときに対応する意味規則の中味を実行すればよい.実現として

は,意味規則番号と各意味規則に対応する手続きを記録した表をもたせるのが一般的

であろう.

属性値の扱いは,再帰下降解析器の場合と同様各記号のインスタンスに対応してレ

コード領域を用意すればよい.ただし今度は再帰手続きの局所変数に頼ることができ

ないので,自前で領域を割り当て/解放する必要がある.ところで,LR解析器では解

析スタックに記号を積んでいた.そこで図 5.2に示すように,解析スタックと平行し

て伸び縮みするもう 1つのスタック (意味スタック — semantic stack)を用意し,解析

スタックに記号を積むのと同時に意味スタックにその記号に対応するレコードを積み,

解析スタックから記号が取り降ろされたら意味スタックからもレコードを取り除くよ

うにする.すると,ある規則 A → X1X2 . . . Xn が還元される瞬間には意味スタック

の上の方にはX1X2 . . . Xnに対応するレコードが積まれていることになる (Xnがトッ

プ)ので,意味規則の中で右辺の記号に対する属性を参照したければ意味スタック上の

対応する場所にあるレコードを参照すればよい.左辺の方の属性はとりあえず別の場

所にレコードを用意し,還元により解析スタックから右辺の記号が取り降ろされた後

左辺の記号が積まれるのに平行して意味スタックに積めばよい.

なお,意味スタック上に直接レコードの領域が取られる場合にはその 1 フレームの

大きさはそこに積まれる全レコードの大きさの最大値である必要がある.レコード領

域は別に取り,意味スタックにはポインタのみを積む,という方法も可能ではある.

次に右辺の途中に意味規則がある場合を考える.1つの方法は,構文を書き換えてそ

の意味規則が最後にくる生成規則を含むようにしてしまうことである.例えば解析を

Page 115: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.4. 構文指示翻訳の実装 115

S1 X1 S2 X2 S3 X3

R3R2R1

意味スタック

解析スタック

記号X1の属性レコード

図 5.2: 意味スタック

しながらただちにコードを生成するコンパイラでは,if文のための条件分岐命令は条

件部のコードの直後に出力される必要がある.それを反映した生成規則の配置は次の

ようになろう.

Stat → "if" Cond "then" { condjump(); } Stat { endif(); }

実際には LR解析器では右辺全体を読み終った時点でないと還元が起こらず,その時

点では condjump()を呼ぶには遅過ぎる.そこで,この規則を次のように変更する.

Stat → IfHead Stat { endif(); }

IfHead → "if" Cond "then" { condjump(); }

こうすれば,"then"が現れた時点でただちに2番目の規則の還元が起き,ここでcondjump()

がよばれるのでうまくいく.

また,もう 1つの方法は意味規則の場所にマーカとよばれる特別な非端記号を導入

して,意味規則はマーカの還元時に起動されるようにすることである.

Stat → "if" Cond "then" Marker-1 Stat { endif(); }

Maker-1 → ε { condjump(); }

この場合にも"then"が現れた直後にMaker-1の還元が起きるのでその時点でcondjump()

が呼ばれるためうまくいくことになる.

いずれの方法でも,一般的なプログラミング言語ではこの変形によって構文解析に

問題が生じることはない.ただし,規則を分割したことでアクセスできる記号 (の属

性)の範囲が変わってしまうという問題はある.例えば前者の方法だと Condの属性は

Page 116: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

116 第 5章 意 味 解 析

変形前には endif() を呼んでいる所で利用できたが,変形後は利用できない (IfHead

の属性を介して渡すように工夫することはできる).

一方,マーカを導入するやり方の場合,スタックの奥にはもともとの規則によって

積まれた記号が埋まっているので,オフセットを調整するだけで (当然,意味規則より

左側にあるものだけだが)もとの記号の属性にアクセスすることができる.

このように,LR解析器における構文指示翻訳の実装は再帰下降解析器の場合に比べ

ると必ずしも直接的でなく,わかりやすいとは言えない.しかし前章で説明したよう

に,LR解析器の利用に際しては構文解析器生成系を使うのが普通なので,その機能の

一環として構文指示翻訳のサポートを組み込むことで十分使いやすくできる.

例えば yaccの場合,先の 2進 10進変換の例を記述すると図 5.3 のようになる. ま

ず,属性値を使用する構文記号に対してはそれぞれがどのようなレコード型に対応す

べきかを%type指定と%union指定の組みにより指定する.次に意味規則中での記述で

あるが,yaccでは各意味規則においてそれが付随している生成規則の左辺の記号を$$,

右辺の記号を左から順に$1,$2,. . .のように記す.また,この例では意味規則が右辺

の最後にしか現れないが,yaccは意味規則が右辺の任意の場所に現れることを許し,

マーカを用いた文法の変形と意味スタックアクセスの調整を自動的にやってくれる.

ところで,この例で使っている文法と意味規則はこれまでに示したものと違ってい

る.これは yaccは LR解析器を生成するので左再帰的な文法が使え,そしてこのよう

な場合には左再帰を使った方が素直に記述できるからである.このため記号 binnumの

len属性は使わないのだが,一応計算だけはするように残してある.

5.5 構文木の生成

ここまでに見てきたように,構文指示翻訳では各意味規則が構文解析の作業とインタ

リーブして実行される.構文解析部自体は構文要素が認識されたタイミングに合せて

意味規則を起動する以上のことは行わないので,意味規則が出力する情報が構文解析

を含むパス (多くのコンパイラではこれが第 1パスになる)の出力となる.先に述べた

ようにこの出力の主たる形態としては目的コード,中間コード,構文木などの選択肢が

ある.前 2者については 9章以降に譲ることにして,ここでは最も基本的な形として

構文木を生成する場合について述べる.

Page 117: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.5. 構文木の生成 117

%type <binnum_attr> binnum;

%type <digit_attr> digit;

%union {

struct { int val } digit_attr;

struct { int len, val } binnum_attr;

}

%%

program :

| program binnum ’\n’

{ printf("\%d\n", $2.val); }

;

binnum :

{ $$.val = 0; $$.len = 0; }

| binnum digit

{ $$.val = $1.val * 2 + $2.val; $$.len = $1.len + 1; }

;

digit : ’0’

{ $$.val = 0; }

| ’1’

{ $$.val = 1; }

;

%%

main() {

yyparse(); }

yylex() {

return getchar(); }

yyerror() {

printf("syntax error.\n"); exit(1); }

図 5.3: yaccにおける意味規則の指定

Page 118: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

118 第 5章 意 味 解 析

Paren

Ident

x

Iconst

3

Ident

y

FactFact

Term

Fact

Plus

Plus

Term

図 5.4: 構文木のデータ構造

通常,計算機内部で木構造を扱う場合にはレコードとポインタを使用し,各レコー

ドを木の節 (枝分かれしている部分)に対応させ,レコードに格納された別のレコード

へのポインタで木の枝を表す.これを構文木に置き換えてみると構文記号ごとに対応

するレコードを割り当て,その決まった位置に記号の種別を示す情報を格納する.また

その記号から導出された右辺の各記号に対応するレコードへのポインタもこのレコー

ドに格納する.例えば

Expr = Term | Term "+" Expr | Term "-" Expr

Term = Fact | Fact "*" Term | Fact "/" Term

Fact = Ident | Iconst | "(" Expr ")"

という文法に従って「(x + 3) * y」という式を解析し,各導出ごとに正直に木の節を

作成したとすると,図 5.4のような構造ができる.このデータ構造を見ると Termとか

Factとか書かれた,単に 1つ下の節を指しているだけの節が多く見られる.これらの

節は Expr = Termとか Term = Factなどの生成規則に対応しているわけだが,Term

とか Factなどの記号は文法を曖昧でなくするために使われただけであり,後の段でそ

れらの情報を保持する必要はない.

Page 119: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.5. 構文木の生成 119

Ident

x

Iconst

3

Ident

y

Plus

Plus

図 5.5: 抽象構文木

そこで普通は生成規則に完全に対応した構文木をつくる代りに,図 5.5のように後

段の処理に必要な情報だけを残すように簡略化したデータ構造を作成する.このよう

に不要な導出に関する情報を省略した構文木を抽象構文木 (abstract syntax tree)とよ

ぶ.また,この考えをさらに進めて*,+など演算子ごとに節の種別を分ける代りに,全

て「2項演算子」のような種類に統一し,別に「どの演算か」という情報を用意する方

法もある.

最後に yaccによるもう少し大きい例題として,四則演算から成る抽象構文木を組み

立ててそれを前置記法で打ち出すというものを示す.まず yaccソースを図 5.6に示す.

なお宣言部には%{と%}で囲んで Cのコードが記述できる.ここでは構文木のノード型

(簡単のため全ての節を同じ形として左側と右側の子供および文字列へのポインタ欄を

もせた)の宣言を置いた.また加算,減算などの「印」の#defineも意味規則の中で参

照するのでここに含める.意味規則そのものはごく直接的に,各生成規則ごとに新し

い節をつくって割り当てるが,ただし「Expr → Term」のような規則の場合は右辺の

属性をそのまま左辺にコピーするだけで新しい節はつくらない.なお,曖昧な文法を

活用すればこの種の規則そのものがかなり不要になるが,ここでは説明のため曖昧で

ない文法を用いた.main,mknodeなどを含む Cソースは図 5.7に示す.字句解析部は

lexで前章と同様につくればよい.実行例を次に示す.

% a.out

1 + 2 ;

(+ 1 2)

1 + 2 + 3 + 4 ;

(+ 1 (+ 2 (+ 3 4)))

Page 120: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

120 第 5章 意 味 解 析

%type <expr_attr> expr;

%type <term_attr> term;

%type <fact_attr> fact;

%token ICONST;

%token IDENT;

%{

typedef struct node {

int kind;

struct node *left, *right;

char *str; } *nodep;

char *copystr();

nodep mknode();

#define T_Plus 1

#define T_Minus 2

#define T_Mult 3

#define T_Divd 4

#define T_Ident 5

#define T_Iconst 6

%}

%union {

struct { nodep tree; } expr_attr;

struct { nodep tree; } term_attr;

struct { nodep tree; } fact_attr;

}

%%

program :

| program expr ’;’ { ptree($2.tree); printf("\n"); }

;

expr : term { $$.tree = $1.tree; }

| term ’+’ expr { $$.tree = mknode(T_Plus, $1.tree, $3.tree, 0); }

| term ’-’ expr { $$.tree = mknode(T_Minus, $1.tree, $3.tree, 0); }

;

term : fact { $$.tree = $1.tree; }

| fact ’*’ term { $$.tree = mknode(T_Mult, $1.tree, $3.tree, 0); }

| fact ’/’ term { $$.tree = mknode(T_Divd, $1.tree, $3.tree, 0); }

;

fact : IDENT { $$.tree = mknode(T_Ident, 0, 0, copystr()); }

| ICONST { $$.tree = mknode(T_Iconst, 0, 0, copystr()); }

| ’(’ expr ’)’ { $$.tree = $2.tree; }

;

図 5.6: yaccによる抽象構文木の組み立て

Page 121: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

5.6. 練 習 問 題 121

2 * 3 + 4 * 5 ;

(+ (* 2 3) (* 4 5))

(((((((a)))))));

a

これだけだと単に中置記法を前置記法に直しているだけに見えるが,実際には中で

いったん抽象構文木が組み立てられ,それを改めてたどりながら出力しているわけで

ある.

5.6 練 習 問 題

5-1. 算術式 (変数の参照と定数および四則演算と括弧があればよい)の値を評価する

属性文法を書いてみよ.ここで,変数への代入を許す (例えば C言語の代入演算

子を入れる)と属性文法をどのように変更しなければならないか.

5-2. 5.4.1項にある C言語による 2進数変換の例を修正して,先頭にマイナス符号を

つけることで負の 2進数も入れられるように直せ.直接好きなようにプログラム

を修正する方法と,まず構文指示翻訳を修正し,それに対応してプログラムを修

正する方法の 2通りでやってみよ.

5-3. 5.4.2項にある yaccによる 2進数変換の例を参考にして,C 言語のような整定数

(0で始まると 8進表記,0xで始まると 16進表記,それ以外なら 10進表記)を

読み込む yacc記述を書け.「最後に文字 H がついていたら 16進数」という規則

だったらどうか.

5-4. 5.5節の例題を参考にして,図 4.1の言語ないしは自分で考案した適当な (あまり

難し過ぎない)言語の抽象構文木を組み立ててS式のような形で打ち出すツール

を yaccによって構成してみよ.

5-5. Lispの S式 (適当に簡略化してよい)を文脈自由文法により構文定義し,それを

読み込んで打ち出すツールを yaccによって構成してみよ.

Page 122: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

122 第 5章 意 味 解 析

#include <stdio.h>

main() { yyparse(); }

yywrap() { return 1; }

yyerror(s) char *s; { fprintf(stderr, "%s\n", s); exit(1); }

#include "y.tab.c"

#include "lex.yy.c"

char *copystr() {

char *p = (char*)malloc(strlen(yytext)+1); strcpy(p, yytext); return p; }

nodep mknode(k, l, r, s)

int k; nodep l, r; char *s; {

nodep p = (nodep)malloc(sizeof(struct node));

p->kind = k; p->left = l; p->right = r; p->str = s; return p; }

ptree(x)

nodep x; {

switch(x->kind) {

case T_Plus: printf(" (+");ptree(x->left);ptree(x->right);printf(")");break;

case T_Minus: printf(" (-");ptree(x->left);ptree(x->right);printf(")");break;

case T_Mult: printf(" (*");ptree(x->left);ptree(x->right);printf(")");break;

case T_Divd: printf(" (/");ptree(x->left);ptree(x->right);printf(")");break;

case T_Ident: case T_Iconst: printf(" %s", x->str);

}

}

図 5.7: mknode,ptreeを含む C言語ソース

Page 123: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

123

第6章 記  号  表

記号表の役割は,ソースコードに含まれる情報のうちで直接中間コードや構文木に反

映されない部分 (具体的には名前や型などの情報)を蓄えることである.一方で,多く

のコンパイラでは名前のスコープや前方参照などの処理も記号表の機能に含めて実現

される.本章では記号表の役割と上記の処理のための実現技術を中心に解説する.

6.1 記号表の位置づけ

コンパイラにおける記号表とは,ソースプログラム中に現れる各々の名前 (識別子)に

関する情報を登録し参照するための機能の総称である.具体的には,記号表に登録さ

れる情報として次のものがあげられる.

(a) 名前の文字列 — どのような名前が現れたか.

(b) その名前のスコープ — 広域名か,どのモジュールの局所名か,等.

(c) その名前の用途 — 変数名,型名,定数名,レコードの欄,等.

(d) その名前に付随する属性 — 変数の型,定数の値,レコード欄の位置,等.

字句解析部で登録される (a)を除けば,これらの情報を抽出するのは全て意味解析部

であり,また意味解析のために記号表を参照することも多いので,記号表は意味解析

部と密接に関わっている.さらに,意味解析より後のフェーズでは記号表を参照する

ことが中心となるので,記号表は意味解析部から後のフェーズへ情報を受け渡す主要

な手段の 1つでもある.

記号表も表の一種には違いないので,1次元配列,2分木,ハッシュ表など一般に使

われている各種の表の実現技法を適用することができる.以下本章ではまず一般的な

Page 124: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

124 第 6章 記  号  表

表の概念と基本的な表の実現について簡潔に述べ,その後でコンパイラの記号表とし

て用いる場合に必要な機能や実現技法に進む.

6.2 表の概念と実現技法

6.2.1 表 の 概 念

表とは基本的には次のような性質をもつデータ構造である.

• 表には<キー,情報>の組を複数個格納できる.

• キーの値を指定してそれと同じキーをもつ組を取り出す (またはそのような組は

格納されていないことを知る)ことができる.この操作を表の検索とよぶ.

加えて特定の組を取り除くなどの操作が必要な場合もある.可能な操作の範囲とそれ

ぞれの効率は表の実現技法に依存する.次に代表的な表の実現技法について解説する.

6.2.2 1次元配列による表

最も単純な表の実現は 1次元配列によるものである.例えば,図 6.1は人名と年齢の

対応表を (文字列と整数の組から成るレコード型の)1次元配列として実現したもので

ある.変数 topはデータがどこまで格納されているかを表している.1次元配列によ

る表は直観的でわかりやすく項目の追加もやさしいが,一方で次の欠点をもつ.

(1) 検索の際には先頭から順にキーの一致を調べていく (線形探索)ので,表に含ま

れている項目数 nに比例した検索時間がかかる.

(2) 項目を削除するときは「穴」ができないように後ろの要素をつめてやる必要があ

る (ただし,一番最後の要素を穴のところに移動してきて topを 1つ分ずらすだ

けでよいなら容易である).

表の項目数が多いと,検索時に項目数に比例した時間がかかるのは大きな欠点となる.

これを改善するために,同じ 1次元配列でもあらかじめ項目をキーの昇順 (降順でも

原理は同じ)に並べておく方法がある.その場合には,2分探索とよばれる技法が使え

Page 125: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.2. 表の概念と実現技法 125

Smith

Jones

Davis

34

28

52

top

図 6.1: 1次元配列による表

る.2分探索では,まず探したい値を,表の中央に格納されている組のキーと比較す

る.それが探したい値と一致すればそれで終りだが,そうでない場合には大小関係で

求める組が表の上半分に入っているか下半分に入っているかを知ることができる.例

えば上半分だったとすれば,今度は探したい値を上半分の区間の中央にある組のキー

と比較する.これを繰り返すたびに探すべき区間の長さは半分ずつになるので,結局

log2n回の比較操作で求む組を見つけるか,そのキーに対応する組は格納されていない

ことがわかる.2分探索を用いると検索時間は短くできるが,その代りに項目を追加/

削除するたびにキーの昇順 (または降順)を保つために平均 n/2個の項目の格納位置を

ずらす必要がある.したがってこの方法はめったに項目の追加/削除がない場合のみ用

いるべきである.項目の追加/削除がある場合には同じ log2nに比例した検索時間をも

つ技法として次に示す 2分木が適している.

6.2.3 2分木による表

2分木とは,図 6.2に示すように,各節が他の 2 つの節へのポインタを保持しているよ

うな (なおかつループをもたない)データ構造である.加えて各節は表に格納されるべ

き情報の 1 項目分を蓄えるものとする.2分木でも前述の 2分探索と同様,ある節の

キーより小さいキー値の節は全てその節の左側にぶら下がり,より大きいキー値のも

のは全て右側にぶら下がるように構成できる.その場合,検索は根元の節から始めて

キー値を比較して求めている値より小さい場合には左側,大きい場合には右側のポイ

ンタをたどることを反復することで行える.したがって検索に必要な時間は木の深さ

に比例し,木がバランス良くできていて偏って深い枝がなければ,log2nに比例する.

2分木では要素 (節)の追加と削除は基本的にその近辺の節のポインタの付替えだけで

行える.したがって動的に要素を追加/削除するのに向いていると言える.ただし 2分

Page 126: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

126 第 6章 記  号  表

Jones 34

Davis 52 Mark 33

Lynn 18 Rob 44

Root

図 6.2: 2分木による表の例

木にも次の問題点がある.

• 挿入/削除の順序によっては偏った木ができて検索効率が悪くなる可能性もある.

• それを避けるように,絶えず木のバランスを調整しておく方法もあるが,その場

合には管理の手間が多少多くなる.

そしてさらに,検索時間が log2nに比例するというのは必ずしも最小ではない.次に

述べるハッシュ表によれば要素数に関係なく一定の時間で検索を行うことができる.

6.2.4 ハ ッ シ ュ 表

例えばキーの値が整数でその範囲が 1~100だったとすれば,最初から大きさ 100の配

列を用意して,キーの値が iの項目はその i番目に入れておくことができる.そうする

と,キーの値からただちに項目の格納場所がわかるので,格納されている項目数に関

係なく一定の時間で検索できるという理想的な表が実現できる.しかし,例えばキー

値の範囲が 1~1,000,000で格納される項目数が 100程度だとしたら,100要素のため

に 1,000,000要素分の配列を用意するのは無駄である.そこで,配列の大きさは 1000

にとどめるために,キー値を 1000で割った余り 0~999を配列中の位置に対応させる

ことができる.

Page 127: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.2. 表の概念と実現技法 127

項目数が 100程度なら,運がよければ配列の同じ位置に入ろうとする項目はなく,「一

定時間で検索できる表」が実現できる.キーが文字列などの場合も,例えばその各文

字のコードを全て掛け合せたあと 1000の剰余を取るといった計算をすればよい.この

ように,キーの値から適当な範囲の整数値を計算するための関数 (ハッシュ関数) を用

意し,それによって計算された位置に項目を格納する,という方式の表をハッシュ表

(hash table)とよぶ.実際には 2つ以上の項目が同じ場所に割り当たることも起こる.

これを衝突とよぶ.衝突をできるだけ減らすため,使用されるキーの値からなるべくば

らついた値が出てくるハッシュ関数を選ぶ (例えば 1000で割るよりはそれに近い素数

で割る方がよい).衝突が起きたときにはそれを処理する方法は基本的に2通りある.

Davis 33

Jones 28

H(Jones)

H(Davis)

H(Mark)Mark 42

図 6.3: ランダムリハッシュによる表

1. ぶつかった場合には片方を「次の場所」に入れる (図 6.3).この場合には検索の

際,ハッシュ関数で計算した場所に別の項目が入っていたら,次の場所,そのま

た次の場所…と調べ,何も格納されていない場所に行き当たったときはじめて

「ない」ことがわかる.本当に次の場所に入れていると,同じハッシュ値をもつ

項目が 3 つ,4つと増えた場合にそれらが表中の「連続したかたまり」として

入ってしまい,それがまた別の衝突を引き起こしやすい.そこで「次」というの

は「ある数 nとびの場所」として,その nを最初とは別のハッシュ関数により計

算する.この方法をランダムリハッシュとよぶ.

2. ハッシュ表本体には直接情報を格納せず,情報は別の節を作成して格納し,その

線形リストの先頭をハッシュ表に入れておく (図 6.4).この場合は衝突が起きた

ときも線形リストを延ばすだけですむが,線形リストの中は線形探索になるので,

衝突が多いと検索速度が低下する.この方法をチェインリハッシュとよぶ.

Page 128: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

128 第 6章 記  号  表

Davis 33

Jones 28

H(Jones)

H(Davis)

H(Mark)

Mark 42

図 6.4: チェインリハッシュによる表

一般に,衝突は項目数に比べてハッシュ表の大きさが小さいときやハッシュ関数の選び

方が不適切なときに多く起きる.特にランダムリハッシュでは格納されるデータ数が

表の容量に近づくとあきを見つけるまでの手間が大きくなり,効率が著しく低下する.

6.3 記号表の特質

6.3.1 文 字 列 領 域

記号表の役割は,まずプログラムの字面上に書かれた名前 (識別子)を同定し,次にそ

れぞれが何のために使われ,どのような属性をもつかを保持することにある.そこで

最初の問題は名前の文字列をどのように格納しておくかという点である.1つの名前

の文字数が数文字程度に限られているなら,図 6.5のように記号表に他の情報と同列

に文字列を格納してよい.しかし,長い名前を許す言語では各エントリごとに最大文

字数を格納できる場所を用意するのは無駄である.

そこで図 6.6のように,名前の文字列を格納する領域は記号表とは別に用意し,同

じ文字列は重複して登録しないように管理し (そのために専用のハッシュ表や 2分木を

使用する),コンパイラの他の部分では文字列の代りに文字列領域のエントリ番号 (あ

るいは格納場所の番地そのもの)を使用する方法が一般的である.このような構成を取

る場合,文字列領域への登録は字句解析部で行うのが素直である.そうすれば構文解

析や意味解析のときに名前の属性として長い文字列をもって回らずにすむ.

文字列領域に要する主記憶の量は通常のコンパイラでは数十 KB程度であまり負担

になることはない.したがって使わなくなった名前の領域を回収する必要はあまりな

Page 129: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.3. 記号表の特質 129

名前文字列 種別

変数

属性1 属性2 …

変数

手続き

x

y

test_proc

最大名前長

図 6.5: 名前文字列を直接格納する記号表

名前ポインタ 種別

変数

属性1 属性2 …

変数

手続き

x y

test_proc

未使用

…文字列領域

図 6.6: 文字列領域を使用する記号表

Page 130: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

130 第 6章 記  号  表

い.ただしコンパイラの開発にごみ集め機能をもつ言語/処理系を使う場合には文字列

はヒープに割り当てられ,使われなくなったときには自動的に回収できる.

6.3.2 ブロック型スコープの処理

スコープ規則 (scope rule)とは,プログラムのある場所で宣言された名前がどの範囲で

有効であり,またある場所で名前を参照した場合,それに対応する宣言はどれかを定

めるものである.これを正しく処理することは前章で述べた名前の同定にあたり,し

たがって意味処理部の仕事であるが,実際には記号表の構造を工夫してこれを実現す

るのが普通である.多くのプログラミング言語ではブロック型のスコープ規則を基本

にしているので,以下ではまずブロック型スコープの実現について述べる.

ブロック型スコープ規則では,プログラムは入れ子状になった複数のブロックから

成り,あるブロックでの名前の定義はそのブロックの内部全体を有効範囲とするが,た

だしそのブロックの内側にある別のブロックが同じ名前に対する別の定義を含む場合

にはその内側ブロックは有効範囲から除かれる (「別の定義」の方が優先する).例え

ば次のようなプログラムの断片を考える.

begin

real a;

...

a := 1.0;

begin

integer a;

...

a := 0;

...

end;

x := a;

...

end

この中には 2つのブロック (beginと endで囲まれている)があり,その両方で aと

いう変数が,ただし外側ブロックでは実数型,内側ブロックでは整数型として宣言さ

れている.ここで 1.0 の代入は,前にある実数型の aの宣言を参照する.次の 0を入

れている方は,内側のブロックに含まれているため整数型の a への代入となる (しか

Page 131: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.3. 記号表の特質 131

し,もし内側のブロックに aという変数の宣言が含まれていなければそれは実数型の

aへの代入になる).次に,最初の endで内側のブロックが終ってしまうと,その中で

宣言された変数はもはや無効であり,最後の aの参照は再び実数型の aを参照する.

ブロック型スコープ規則を記号表の立場から見ると,プログラム上である名前が出

現したとき,その場所を囲む全てのブロックのどれかでその名前が定義されていれば

その定義が見つかるようにすればよい.ただし,そのような名前が複数あった場合に

は,それらを定義しているブロックのうち一番内側のものにおける定義が取れなけれ

ばならない.一方,ある場所で名前に対する定義を登録しようとした場合,一番内側

のブロックにおいてすでに同じ名前が定義されていれば 2重定義の誤りとなるが,外

側のブロックで同じ名前が定義されていればそれは誤りではない.

a integer

a real

最内側ブロックに対応する記号表

最外側ブロックに対応する記号表x real

図 6.7: ブロック型スコープ規則の実現 (1)

ブロック型スコープ規則を実現する方法として,各ブロックごとに別の記号表を用

意するやり方と 1つの表で対処するやり方がある.ブロックごとに別の表を用意する

場合は,ブロックの入れ子の深さが nのとき n個の記号表が存在する.そして,図 6.7

のように探索においては最内側のブロックに対応する表から始めて目的の名前が見つ

かるまで順に表を取り替えながら探す.また登録に際しては最内側の表に衝突する定

義がなければただちにそこへ登録してよい.この方法で探索に用いる表のリストは,新

しいブロックに入るときそのブロックに対応する表を先頭に追加し,ブロックから出

るときには対応する表 (リストの先頭にある)を取り除くことになる.

1つの表ですませる場合もブロック型スコープ規則の「一番新しいスコープが一番最

Page 132: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

132 第 6章 記  号  表

初に閉じられる」性質を利用する.例えば記号表が単純な 1 次元配列で実現されてい

る場合,図 6.8のようにブロックに入るごとに現在表のどこまで定義が入っているか

の「印」をつけ,定義の登録は常に表の最後に追加する.検索においては表の末尾か

ら前に向かって探索し,最初に見つかったものが最内側の定義に対応する.登録の際

には「印」より手前に同名のものがなければ 2重定義ではない.ブロックから出る際

には「印」よりあとにある定義を捨ててしまえばよい.ハッシュ表でチェインリハッ

シュを使用する場合には各リストごとに同様の処理を行うことができる.

a integer

a real

x real

上から検索

内から2番目のブロックに対応する「印」

最外側のブロックに対応する「印」

図 6.8: ブロック型スコープ規則の実現 (2)

整列した表,ランダムリハッシュ,2分木などによる記号表の場合でも,「印」の代り

に各エントリに現在のブロックの深さを示す数値を入れておき,探索においては同名

のエントリ全部を見つけたあとこの数値が最大のものを取り,登録においては現レベ

ルと同じ数値をもつエントリがなければ追加を許すことで 1つの表による実現が行え

る.しかし,この方法ではブロックを出るときそのレベルのエントリを全部探して取

り除く手間がかかり,あまり得策でない.

また,スコープを抜けても,後のパスでの参照やデバッガ用出力を考えると (単純な

1パスコンパイラでない限り)そのスコープでの定義情報全てを捨ててしまえるわけで

はない.したがって 1つの表で間に合せる場合には定義情報は別の領域に保持し,表

には検索用のポインタのみを格納する.複数の表をリンクする場合にはスコープを抜

けた後もそのスコープに対応する表を保持しておけるので,この点では有利である.

Page 133: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.3. 記号表の特質 133

6.3.3 スコープ規則の拡張

新しい言語では単純なブロック型スコープ以外にモジュール機構やデータ抽象機構な

どと関連して込み入ったスコープをもつものが多い.以下ではスコープごとに別の記

号表を使用する方針に絞ってモジュール機構などによるスコープ規則の拡張について

解説する.モジュール機構とは複数の対象物 (変数,定数,型,手続きなど)をグルー

プとしてまとめて名前を付けられるようにした機構のことである.これを記号表の立

場から見ると,従来のブロック型スコープが名前をもたず,したがってスコープが閉

じてしまうと以後はアクセスできなくなるのとは対照的に,名前をもち,スコープが,

閉じた後でもその名前を通じてアクセスできるものがモジュールである,といえる.こ

の点を除けば,名前を定義する際にモジュールと普通のブロックの区別は特にない.言

語によってはモジュールの中に別のモジュールを入れ子にできたりもするが,これも

内側のモジュールの名前が外側のモジュールの記号表に登録されるというだけであり,

各時点で最内側の記号表に定義を登録する点にかわりはない.

一方,名前を参照する立場に立ってみると,モジュールの導入に伴って様々な変化が

必要になる.まず,モジュールの要素全てがモジュールの外から参照できる (公開され

ている)わけではない,という点があげられる.公開の指定方法は言語によって export

句のようなもので指定する,モジュールの構文として外からの参照を許す定義部と許

さない定義部に分かれている,など様々であるが,記号表では図 6.9のように各名前

についてそれがどちらの範ちゅうに属するかを覚えておけばよい.そして,モジュー

ル外からのアクセスには,この情報を参照してOKの場合だけ検索を許す.

外部からの参照は,基本的には「モジュール名.要素名」のような構文でモジュー

ルを明示してその要素にアクセスする.この場合モジュール名は他の名前と同様に現

スコープに対応する記号表のリストを通じてアクセスし,見つかったエントリからモ

ジュールの記号表を取り出してそのうえで要素名を検索すればよい.言語によっては,

モジュール名を毎回明示するのは煩わしいとの考えから,どのモジュール (のどんな名

前) を使用する,という宣言をしておけば要素名のみでアクセスを許すものがある.こ

の場合は,宣言がなされた時点で現スコープの記号表にこれらの名前を登録してしま

う方法と,参照用の記号表リストの先頭にモジュールの記号表をつけ加える,という方

法が考えられる.前者は概念的にはやさしいが,モジュールが多数の公開要素をもつ

場合それら全てを登録する負担が大きい.一方,後者は既存の記号表をリストに加え

Page 134: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

134 第 6章 記  号  表

module A exports X, Y, Z ...end A

X 公開公開

公開

YPZQ

モジュールの記号表

A

全体の記号表

図 6.9: モジュールに対応する記号表の構造 (1)

るだけですむが,宣言が「どの名前を...」という情報を含む場合には一時的にモジュー

ル記号表の情報を書き換えて宣言に現れた名前のみをアクセス可能にし,スコープの

終りで書き換えた情報を復元するという管理が必要になる.

また,レコード型についてもその欄の名前は同じレコード内で重複してさえいなけ

れば,他の関数名や変数名,他のレコードの欄名とは同じでもかまわないのが普通で

ある.レコード型を統一的に扱う1つの方法は,レコードをその全要素が公開されてい

るような一種のモジュールであると考え,上で説明した方法を適用することである.例

えばレコードの欄の指定は概ね「変数名.欄名」のような形をしているが,変数の型か

らどのレコード型かわかるので対応する記号表で欄名を検索すればよい.また Pascal

の with文などはその内部では欄名が変数名と同様に扱えるようにする効果をもつが,

これはモジュール名を前置せずに要素をアクセスする宣言に相当する.その実現方法

としては先に述べた 2 通りの方法のどちらでも使用できる.

Page 135: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.4. 記号表に関連する言語処理系の問題 135

6.4 記号表に関連する言語処理系の問題

6.4.1 前方参照の問題

ここまででは,ブロック構造のスコープであると否とを問わず,プログラム中に現れ

る名前はまず最初に宣言され,その後で参照されることを前提としてきた (Fortranの

ように暗黙の変数宣言を許す言語では変数が最初に参照される所が宣言を兼ねている

と考えればよい).しかし,宣言よりも参照が先に現れることが許される例もある.例

えば,多くの言語ではラベルはそれが書かれている場所ではじめて宣言される.した

がって

goto L;

...

L: ...

は参照よりも後に宣言がある,前方参照ということになる.

前方参照を処理する基本的な方針としては,パスを分けて処理する方法と 1つのパ

ス内で処理する方法がある.パスを分ける場合は概念としては簡単で,最初のパスで

宣言のみを処理し,後のパスではその情報は記号表に載っているので普通に処理する.

一方 1つのパスで処理する場合には参照が現れたときにその名前が記号表になければ

前方参照である旨と合せて登録し,その名前があるべきスコープが閉じるときに依然

として宣言がなければ誤りを報告することになる.

この場合,宣言より前の時点で現れた参照において,宣言時にならないとわからない

情報を必要とする場合が問題である.例えば上の gotoの例ではラベルの番地はそのラ

ベルの場所に来ないとわからないので,直接機械語命令を生成するコンパイラの場合

jump命令のオペランドが生成できない.このような場合はとりあえずオペランドの場

所は空けたまま命令を生成し,後でラベルが現れた時点で空けたままになっているオ

ペランド欄に確定した番地を書き込んで回る,といった技法 (後埋め— backpatching)

が必要になる.

また,例えば型定義などの場合には複数の相互依存する型を定義することがよくあ

るので,それに伴って必然的に前方参照が発生する.例えば次の例が典型的である.

type list = ^cell;

cell = record

Page 136: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

136 第 6章 記  号  表

car, cdr: list

end;

型定義の処理全体を複数パスにするのはあまりに大げさなので,この場合も何とか

工夫して 1つのパスですませるのが普通である.例えば Pascalでは型定義における前

方参照をポインタ型のみに限っているので,前方参照の定義が出てきた場合には「と

りあえずポインタ」とだけ記録して先へ進み,参照されている定義が出てきたところ

でその情報を埋めるようにできる.通常,ポインタ値の格納に必要な領域の大きさは

何型へのポインタでも同じなので,このような前方参照を許してもそれを含むレコー

ドや配列の大きさが計算できない,といった困難は生じない.

このように,多くの言語では言語仕様としてコンパイラに都合の良い程度に前方参

照を制約している.しかし,言語によっては言語仕様の制約がコンパイラの自然な実

現に合致しないこともある.Pascalで次の例を考えよう.

type a = real;

...

procedure p;

type b = a;

a = integer;

...

この場合,bは整数であるべきか,実数であるべきか.正解は,bの定義は前方にある

aの定義 (そのスコープは手続き p全体である)を参照しており,したがってエラーで

ある,というものだが,bの定義の時点で記号表を参照して最内側の定義をもってく

る,という素直な実現だとこのエラーは発見され損なう.これを発見するためには,b

の定義が外側の aを参照したことをずっと覚えておき,型定義部の終りで再定義され

たものへの参照をチェックする必要がある.実は,この面倒は言語仕様が次のどちらか

であれば不要である.

• 名前のスコープはその名前が定義された箇所からそのブロックの最後までである.

• 名前のスコープ内でそれと同じ名前を再定義することは (内側ブロックでも)許

さない.

前者の場合は,先の bの定義は外側の aのスコープに属するので,素直な実現のまま

で正しい.一方,後者ではそもそも aを再定義できないのでこの種の問題は生じない.

Page 137: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.4. 記号表に関連する言語処理系の問題 137

6.4.2 分 割 翻 訳

記号表に関連したもう 1つの話題として,分割翻訳 (separate compilation)があげら

れる.分割翻訳とは,プログラムをモジュールなどの単位で複数のファイルに分けて

別個に翻訳し,リンカで 1つにまとめて完成させることを言う.ただし狭い意味で分

割翻訳と言った場合,別個に翻訳されるモジュール間でも型検査が正しく行えるよう

な機構を備えることを意味する.これに対し,モジュールを独立に翻訳できるだけで,

整合性についてはプログラマの責任に任されている場合には独立翻訳 (independent

compilation)とよんで区別する.Cや C++の処理系は基本的に独立翻訳であるが,た

だし関数,型,広域変数などの宣言を 1つのファイルにまとめ,独立に翻訳される各

ファイル中でこの宣言ファイルを取り込むよう指定することで整合性を保つ.これで

もプログラマ任せよりはだいぶましであるが,宣言ファイルの構成や取込み指定は依

然としてプログラマの責任である.これに対し,AdaやModula-2などの言語はもと

もと分割翻訳を前提としており,モジュールにまたがった検査は処理系が自動的に行

うことが前提となっている.

分割翻訳を行う場合,あるモジュールに対する情報定義と整合性検査のための情報

参照が翻訳単位の境界を越えて行われることに主要な難しさがある.これらの情報そ

のものは先に述べたモジュールに対応する記号表に他ならない.翻訳単位を越えて情

報を流通するためこれらの情報をいったん外部ファイルに書き出し,必要なときに読み

込むことになる.このとき,そのファイルとコンパイラが扱うモジュールとの対応をつ

けることも簡単ではない.例えば「モジュール名と同じ名前のファイルに書く」ように

しようと思っても,ファイル名の長さに制約があるため不可能かもしれない.また別

の問題として,あるプログラムと別のプログラムでたまたま同名前だが中身は全然違

うモジュールが出現するかもしれないし,一方で複数のプログラムで 1つのモジュー

ルを共通に利用するかもしれない.さらに,そのようなモジュールを改訂することも

考える必要がある.変更の内容によっては,それを参照している他のモジュールを翻

訳し直さないといけないかもしれない.

また,書き出す情報にしても問題の記号表のみを書き出すだけでは通常不十分であ

る.というのは,例えば記号表中で名前が文字列領域へのポインタとして表現されてい

たら,そのポインタ値をそのままファイルに書いても役に立たない.またあるモジュー

ルの記号表に現れる型がまた別のモジュールが定義しているものであったりすること

Page 138: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

138 第 6章 記  号  表

もある.言い換えれば,ファイルにはコンパイラの記号表に含まれる情報全てが意味

をもって復元できるに足るだけの情報が書かれなければならず,しかも複数のコンパ

イラ起動全体を通じて 1つないし複数のプログラム全体として整合性をもった情報が

組み立てられる必要がある.このような情報全体を「コンパイル環境情報」「モジュー

ルデータベース」などとよぶこともある.

6.5 練 習 問 題

6-1. 手近にあるなるべく大きいプログラムのソースファイルを 1つ選び,その全ての

名前の文字列を文字列領域に蓄えたとしたらどれだけの領域が必要か予想し,そ

の後実際に計算して調べてみよ.調べるには適当なツールを使っても,実際に字

句解析しながら文字列領域に名前を詰めていくプログラムをつくってもよい.

6-2. 前章の練習問題で作成した,図 4.1の言語の抽象構文木を生成するコードに手を

加え,変数宣言を処理するように直してみよ.例えば変数宣言は「var 変数名;」

の形としてプログラムの先頭にまとめて現れるものとし,変数の 2重定義と未定

義変数の使用を検出できるようにする.検査するついでに抽象構文木の上では変

数は全て一意的な変数番号で表すように直せ (出力の形式も変数番号がわかるよ

うなものに直すこと).

6-3. 前問のプログラムをさらに改良して,ブロック型スコープを実装せよ.例えば

beginから endまでを 1つのブロックとし,ブロックの先頭にはそのブロック

の局所変数宣言をいくつでも置いてよいものとする.この場合プログラム全体も

1つのブロックにするのが素直であろう.正しくスコープが処理できたかどうか

は,出力が変数番号による表示になっていれば容易にチェックできる.

6-4. 前問のプログラムを「宣言はブロックの途中にも書け,なおかつ変数のスコープ

は宣言箇所からそれを含む最内側ブロック末尾まで」という規則になるよう直し

てみよ.いくつかプログラム例を記述し,この方式がよいかどうか考えてみよ.

6-5. 前問のプログラムにモジュール機構を組み込め.例えば各モジュールは手続きで

あり,call文によって呼べるが,モジュールから exportされた名前はモジュー

Page 139: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

6.5. 練 習 問 題 139

ル名を前置することによって他のモジュールから参照でき,これによって値を受

け渡せるようにする.つまり次のようなプログラムが書けるわけである.

module add;

export x, y, res; var z; z = x + y; res = z; end;

module main;

read a; read b; add.x = a; add.y = b; call add; print add.res; end;

Page 140: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス
Page 141: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

141

第7章 型  検  査

初期のプログラミング言語においては,コンパイラが行う型に関する処理もごく簡単

なものであった.しかし現在では,複雑なデータ構造の記述や抽象データ型などの採

用に伴い,コンパイラにおける型の処理は込み入ったものとなってきている.本章で

はプログラミング言語における型の機能とそれを実現するためのコンパイラの機能に

ついて述べる.

7.1 型の役割りと位置づけ

プログラミング言語における型の役割としては,次のようなものがあげられる.

(a) 複数のデータの種類を使い分ける.

(b) 様々なデータ構造の構成を可能にする.

(c) プログラムの安全性を高める.

(d) プログラムの効率を高める.

(e) プログラムの書きやすさ/読みやすさを高める.

歴史的に見て,プログラミング言語における型の扱いはおおよそこの順番に従って進

歩してきた.(a)まず,整数型と実数型のデータでは使用する機械語命令が異なるので,

その使い分けのためデータの種類を区別することは初期のコンパイラでも必須の機能

であった.(b)次の段階として,配列やレコードのようなデータ構造が使われるように

なるにつれ,それらの区別も型の区別の一種として統一的に扱うことが一般的になっ

た.(c)並行して,互いに異なる型どうしの演算を誤りとして検出することの利点が認

識されるようになった.(d)現在ではそれと対極をなす,動的な型の言語 1.も多く使

1実行時にならないとデータの型が決定できないような言語をいう.

Page 142: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

142 第 7章 型  検  査

われているが,それらの言語処理系における実行時の型管理のオーバヘッドが認識さ

れるとともに,静的な型情報を用いることでこれらのオーバヘッドを回避できる点も

再認識されるようになった.(e)また全体を通じて,型はプログラマの思考を助ける手

段でもあるという考え方も現在では一般的なものである.以下本章では (e)は別格と

して (a)~(d)を具体化するためのコンパイラの機能と実現について述べる.

7.2 型 の 同 一 性

2つの型についてそれが同一のものかどうかを決めることは,一見やさしそうに思われ

るが,実はいくつかの問題を含んでいる.次の例を考えてみる.

type point = record x, y: real end;

var a: point;

b: point;

c: record x, y: real end;

aと bの型は同じだが,では aと cではどうか.実は「同じ」という立場も「違う」

という立場も可能である.まず「違う」方から考えてみる.その論拠は「最初の型定義

は pointという『新しい型』をつくっているのであり,その目的は『点』を表す値を

他の値から区別することにある.したがってたまたま同じ構造のレコードであっても

それは pointとは別ものだ」ということである.この方針を名前による同一性 (name

equivalence)とよぶ 2.名前をつけない型構成式を書いた場合には,他のどれとも異な

る無名の名前がつくと考える.したがって,名前による同一性を採用した場合はソー

スプログラム上に型構成式が現れるごとに 1つの新しい型がつくられる.例えば

var a, b: record x, y: real end;

c: record x, y: real end;

とあった場合には aと bは同じ型だが cはこれらとは別個の型になる.

次に,「同じである」という方針を考える.その論拠は「2つの型構成式でつくられ

る型の値に対する操作や,そのメモリ上の表現は全て等しく,一方の値の代りに他方

の値を使っても不当な操作は起きないから」である.これは 2つの型の構造が同じな

2この呼称は型に名前をつけるごとにそれぞれ別個の新しい型ができることによっている.

Page 143: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

7.2. 型 の 同 一 性 143

らそれらは同じ型という方針であり,構造による同一性 (structural equivalence)とよ

ぶ.先の例ではレコードの欄名は全て同じだったが,欄名の差異を考慮する主義とし

ない主義がある.とりあえず構造のみを問題にするとして,次のはどうか.

type p1 = ^r1;

r1 = record r:real; next: p1 end;

p2 = ^r2;

r2 = record r:real; next: p2 end;

p1と p2,r1と r2を重ねると両者の定義が一致するから,それぞれは同じ型であろう.

では

type p3 = ^r3;

p4 = ^r4;

r3 = record r:real; next: p4 end;

r4 = record r:real; next: p3 end;

p5 = ^r5;

p6 = ^r6;

r5 = record r:real; next: p6 end;

r6 = record r:real; next: p5 end;

はどうか.一見,r3と r5,p3と p5,r4 と r6,p4と p6がそれぞれ同じ型のように

思える.しかし,r3と r6,r4と r5等を重ねて悪い理由もない.実は r3と r4を重ね

ることもでき,実際そうすべきである.したがってこれらは全て同じ型であり,そし

て先の r1,r2とも同じ型である.整理すると,構造による同一性の判定は一群の型を

うまく重ね合せる単一化 (unification)が存在するかどうかの判定になる.

このような判定を行う手順を Lispで記述してみる.簡単のため型構成子はポインタ

とレコードしかなく,レコードには欄が 2つしかなく,欄名は書かず,全ての型定義

には名前を与えるものとする.上の r1~r6の例はすでにそうなっている.これを適宜

S式で表し,変数*def-list*に入れておく.

(setq *def-list*

’((p1 (pointer r1))

(r1 (record integer p1))

(p2 (pointer r2))

(r2 (record integer p2))

(p3 (pointer r3))

(p4 (pointer r4))

Page 144: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

144 第 7章 型  検  査

(r3 (record integer p4))

(r4 (record integer p3))))

名前のついた型それぞれについて,それが基本型か定義された型かを知るために属

性 atomicを用いる.ここでは基本型は integer のみである.

(defmacro t-atomic (x) ‘(get ,x ’atomic))

(setf (t-atomic ’integer) t)

定義された型については,その定義内容を次の find-defにより検索できる.

(defun find-def (x)

(for l *def-list*

(if (eq (car l) x) (return-from find-def (cadr l)))))

次に,「どの型とどの型は実は同じか」という情報を保持するため*equiv-list*とい

う変数を用いる.最初の状態では各型名はそれ自身とのみ同じ,としておく.

(setq *equiv-list*

’((integer) (p1) (r1) (p2) (r2) (p3) (p4) (r3) (r4)))

そして find-equivは与えた型名と同じ型の集合を返す.

(defun find-equiv (x)

(for l *equiv-list*

(if (member x l) (return-from find-equiv l))))

また joinは 2群の (それぞれ互いに等しい)型の集合が,全体としてまた同一の型

であるということを記録する.

(defun join (l1 l2) (push (append l1 l2) *equiv-list*))

これは単に両集合を合せたものを*equiv-list*の頭につけ加えるだけで古いものを削

除しないが,find-equivは探すときに頭から探すので常に新しい方が見つかる.単一

化を試みる関数 unifyは次の通り.

(defun unify (t1 t2 &aux l1 l2)

(setq l1 (find-equiv t1)) (setq t1 (car l1))

(setq l2 (find-equiv t2)) (setq t2 (car l2)) ; (1)

(cond ((eq t1 t2) t) ; (2)

Page 145: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

7.2. 型 の 同 一 性 145

((or (atomic t1) (atomic t1)) nil) ; (3)

(t (setq d1 (find-def t1)) (setq d2 (find-def t2)) ; (4)

(cond ((not (eq (car d1) (car d2))) nil) ; (5)

((eq (car d1) ’pointer) ; (6)

(join l1 l2) (unify (cadr d1) (cadr d2)))

((eq (car d1) ’record) ; (7)

(join l1 l2) (and (unify (cadr d1) (cadr d2))

(unify (caddr d1) (caddr d2)))))))

(1)まず渡された 2つの型名について,それと同じものの集合を求め,以下ではその先

頭の型名を代表として使う.(2)もしそれらが等しければ両方の型は同じだが,(3)型

名が等しくなく,一方が基本型なら等しくはない (ここでは基本型に別名をつけること

を許していない).(4)それ以外というのは両方とも定義された型の場合である.そこ

でまずその定義を求め,(5)その構成子 (ポインタかレコードのはず)が互いに等しく

ないならこれらの型は等しくない.(6)ともにポインタなら,両者をとりあえず等しい

ものとして,その中の型が等しいかどうか調べる.(7)ともにレコードなら,両者をと

りあえず等しいとして,その中の 2つの欄がそれぞれ等しいかどうか調べる.そのま

ま進んで最後までつじつまが合ったら本当に等しかったことになる 3.r1と r4が等し

いかどうか調べる実行例を示す.

>(unify ’r1 ’r4)

T

>*equiv-list*

((P1 P3 P4) (R1 R4 R3) (P1 P3) (R1 R4) (INTEGER) (P1) (R1) (P2) (R2)

(P3) (P4) (R3) (R4))

*equiv-list*を見ると,とりあえず r1と r4,p1と p3が同じだとし,そこからさら

に進んで r3や p4もこれらと同じだとわかった経緯が読みとれる.

3本当は仮定が間違っていたときそれを取り消すべきなのだがそれは略した.だから一度失敗させると再度ロードする必要がある.

Page 146: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

146 第 7章 型  検  査

7.3 宣 言 の 処 理

7.3.1 型指定の処理

意味解析部が宣言を処理する際,まず必要なのはソースプログラム中の各型指定を,そ

れぞれの型を一意に表すような識別子 (型番号のようなもの)に変換することである.

そのために一般には型表 (type table)を用意する.型表にはプログラム中に現れる全

ての型を登録するが,同じ型にはちょうど 1つのエントリが対応するようにしておく.

そうすればエントリ番号を一意的な識別子として用いることができる.

具体的には,型表には最初は言語で初めから定義されている標準の型のエントリの

みを入れておき,新しい型が出てくるごとに新しいエントリを割り当てる.型に名前

がついている場合には,その名前は普通の記号表に入れ,記号表のエントリに型番号

を入れる.型表の方にはコンパイラの処理としては名前を保持する必要はないのだが,

型の情報を打ち出すときには名前がわからないと困るので,その場合には型表のエン

トリにも名前の文字列を入れるか,または記号表のエントリへのポインタを保持させ

る.ただし 1つの型に複数の名前がついていることもあるのでどれか 1つで代表させ

るなどの処置が必要である.名前による同一性を採用する場合は型生成子が出てくる

ごとに新しいエントリを割り当てればよい.一方,構造による同一性の場合はいった

ん登録した後で実はそれが既存のエントリと単一化できるかどうか調べ,できる場合

には既存の方で置き換える.

7.3.2 宣言部の処理

宣言部の処理の基本は,宣言される名前ごとにそれを属性情報とともに記号表に登録

することである.型名,変数名,定数名などはその種別とともに対応する型ないし値

を登録すればよい.モジュール名の場合は,前章で述べたようにモジュール自体が 1

つの閉じた記号表に対応するので,この記号表をモジュール名とともに登録する.

最後に関数と手続き (以下関数で代表させる)がある.関数に付随する情報としては

受け取る引数の型と種別 (受渡し機構など),戻り値の個数と型,例外の種別などの情

報がある.そこで,(言語仕様上はそうなっていなくても)これらの情報を 1つの型 (関

数型)として扱い,型表に登録する方がよいかもしれない.

Page 147: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

7.4. 式における型の同定 147

関数に関する情報は,関数宣言から得られる場合と関数定義から抽出される場合と

がある.ソースプログラムの後の方で定義される関数を呼ぶ場合にはあらかじめ宣言

を要求するのが普通なので,このような場合には 1つのソースプログラム中に宣言と

定義の双方が存在し,それらが矛盾しないか検査する必要が生じる.宣言と定義が別

の翻訳単位に分かれている場合には (言語仕様が要求するならば)前章で述べたような

分割翻訳をまたがる検査を行う.

言語によっては 1つの関数名が引数,戻り値の個数や型が異なる複数の関数定義に

対応することを許す.これを多重定義 (overloading)とよぶ.多重定義される各関数が

常に互いに異なるモジュールに属するなら,モジュールごとの記号表にそれぞれの定

義を登録するだけなので問題はない.一方,C++やAdaのようにモジュールとは独立

に多重定義関数がつくれる場合には,1つの記号表に複数の同名の関数が登録される

ことを許すか,または記号表の 1つのエントリが複数の関数定義に対応することを許

すようにしなければならない.

多くの言語では"*"という演算子は整数の乗算と実数の乗算の両方を表すが,これも

多重定義である.利用者が多重定義関数を書くことを許さない言語であればこれらを

特別扱いで処理してもよいが,演算子と関数を合せて多重定義を許すような扱いをす

る方がコンパイラ内部の処理としては統一性が取れる.

7.4 式における型の同定

宣言部から抽出した情報をもとに各式の型を決定し,型に関する誤りがあればそれを

検出することも意味解析部の主要な仕事である.これは基本的には,式を表す抽象構

文木の葉 (定数名,変数名,リテラルなどであり,その型は記号表を見ればわかる)か

ら始めて上に向かって型を割り当てていけばよい.多重定義がない場合には,演算子

や関数呼出しに対してはオペランドや引数の型と個数をチェックし,その節の型は演算

子や関数の結果の型とする.また自動的な型変換規則 (例えば実数と整数を足すときは

整数を自動的に実数に変換する等)がある場合には,該当する場所に変換のための組込

み関数の呼出しを挿入する.

多重定義がある場合でも演算子や関数の選択が引数のみによって決まる場合 (ほとん

どの言語はそうである)には,抽象構文木の下から型を割り当てていけば呼出しの節で

Page 148: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

148 第 7章 型  検  査

は全ての引数の型が決定されており,これに基づきどの演算子ないし関数かを選択で

きる.このように葉から根に向かって順に型を決定できる言語であれば,上向き構文

解析と並行して型の同定が行えることになる.問題はAdaのように「この式を受け取

る側でどんな型を欲しがっているか」という情報まで多重定義の選択に参加する場合

である.このときは次の 2つの処理方法がある.

(a) 構文木を「この部分はどんな型であって欲しいか」という情報を伝搬させながら

上から下へ向かってたどり,下まで到達したらこの情報と記号表の情報に基づき

各部分の型を割り当てながら上へ戻ってくる.この方法は再帰手続きで素直に実

現できるが,構文木が完成した後で動作させることになる.

(b) 構文木の下から上に向かって,各節に可能な型全てを割り当てていく.その際,

各節で下から登ってきた型のうち,その場所で必要とされる型 (の集合)に合わ

ないものは捨てられる.この方法では上向き構文解析と組み合せれば実際に抽象

構文木を組み立てることなく型の割当てが行える.その代りに,途中の段階で割

り当て方が複数存在する場合を扱う必要がある.

いずれの方法でも最終的に可能な型の割当てが 1通りでなければ誤りである.(a)では

割当てのどこか途中で可能な割当てが複数あったらその時点で誤り,(b)では一番上の

節まで来てそこに割り当てられた型が 1 つでないとき誤りとする.

実際にこれらを行うLispのコードを示す.以下では簡単のため,全ての演算子と関

数は 2引数で結果を 1個返すものとする.変数*op-dcls*にそれらの情報を次のよう

に入れておく.ここでは代入,ベキ乗,入力 2種の 4つを用意した.

(setq *op-dcls*

’((= (real int) real)

(= (real real) real)

(power (real real) real)

(power (real int) real)

(power (int int) int)

(get (int int) int)

(get (int int) real)

(getint (int int) int)))

変数とその型の情報は*var-dcls*に入れ,これらを検索する関数find-opとfind-var

も用意する.

Page 149: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

7.4. 式における型の同定 149

(setq *var-dcls* ’((a real) (i int)))

(defun find-op (o &aux r)

(for x *op-dcls* (if (eq (car x) o) (push (cdr x) r)))

r)

(defun find-var (v)

(for x *var-dcls*

(if (eq (car x) v) (return-from find-var (cadr x)))))

find-opは与えられた名前と一致するものを全て集めてリストとして返す.これを

用いて,まず上記 (a)の方針による型割当てを行う関数 entypeaを用意する.この関

数は式を表す抽象構文木 (Lispだから前置記法)と,その木がどの型であって欲しいか

という情報を受け取り,その木の各部に型を表す記号を付加する.うまくいかないと

きは型の代りに nilを付加し,その下は触らない.

(defun entypea (e x &aux y a b r)

(cond ((atom e) (setq y (find-var e))

(if (eq x y) (list y e) (list nil e))) ; (1)

(t (for z (find-op (car e)) ; (2)

(cond ((eq (cadr z) x) ; (3)

(setq a (entypea (cadr e) (caar z)))

(setq b (entypea (caddr e) (cadar z)))

(if (and (car a) (car b)) ; (4)

(push (list x (car e) a b) r)))))

(if (eql (length r) 1) (car r) (cons nil e))))) ; (5)

(1)もし部分木が記号なら変数だから find-varで型を求め,それが望ましい型と一致

したら (型 記号)の形の S式を返す.そうでなければ型の代りに nilをつけたものを

返す.(2)それ以外は部分木は演算子や関数を表すので find-opでその名前をもつ多

重定義された演算子群/関数群の引数と戻り値の型の情報を全部集め,それぞれについ

て (3) 戻り値の型が要求されている型と等しい場合のみその第 1引数と第 2引数につ

いて,再帰的に型の割当てを行い,(4)ともに成功した場合 (というのは,nilでない

型が先頭について来ることで表される)にはそれを変数 rで覚える.(5)以上を多重定

義された全部について実行し終ったとき,rに覚えているものがちょうど 1つであれ

ば OKなのでそれを返す.そうでなければ失敗なので nilをつけて返す.実行例は次

の通り.

>(entypea ’(= a (power (get i i) a)) ’real)

Page 150: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

150 第 7章 型  検  査

(REAL = (REAL A) (REAL POWER (REAL GET (INT I) (INT I)) (REAL A)))

>(entypea ’(= a (power (get i i) i)) ’real)

(NIL = A (POWER (GET I I) I))

>(entypea ’(= a (power (getint i i) i)) ’real)

(REAL = (REAL A) (INT POWER (INT GETINT (INT I) (INT I)) (INT I)))

最初の例は Ada ふうに書けば「a := power(get(i, i), a)」に相当する.この

場合,aが realだから多重定義された powerのうちで適用可能なのは power(real,

real)だけで,したがって getも realを返す方だけが選べる.実行結果でも確かにそ

のようになっている.ところが,2番目 (「a := power(get(i, i), i)」に相当)で

は power(real, int)と power(int, int)の両方が可能なため getも両方適用でき

てしまい,選択が一意に決まらず割当てに失敗する.ここで 3番目のように getを整

数しか返さない getintに置き換えると power(int, int)のみに決まるので再びうま

くいく.次に (b)のやり方に従うコードを示す.

(defun entypeb (e &aux y a b a1 b1 r)

(cond ((atom e) (setq y (find-var e))

(if (null y) nil (list (list y e)))) ; (1)

(t (setq a (entypeb (cadr e))) (setq b (entypeb (caddr e))) ; (2)

(for z (find-op (car e))

(setq a1 (assoc (caar z) a))

(setq b1 (assoc (cadar z) b)) ; (3)

(if (and a1 b1)

(push (list (cadr z) (car e) a1 b1) r))) ; (4)

r))) ; (5)

(1)まず相手が変数なら find-varで探した型をつけたもののみから成る集合を返す.

(2)一方演算子等の場合には,まずその両引数の型割当てを求める.それぞれ複数求ま

るかもしれない.(3)次に,多重定義された引数と戻り値の型の情報を全部集め,それ

ぞれについてその第 1,第 2引数の型がさっき求めた両引数の割当て集合の中にあるか

どうか探す.(4)もし両方ともあれば,この割当てを集合 r に追加.(5)全部調べ終っ

たら rに集めたものをそのまま返す.この実行例は次の通り.

>(entypeb ’(= a (power (get i i) a)))

((REAL = (REAL A) (REAL POWER (REAL GET (INT I) (INT I)) (REAL A))))

>(entypeb ’(= a (power (get i i) i)))

((REAL = (REAL A) (INT POWER (INT GET (INT I) (INT I)) (INT I)))

(REAL = (REAL A) (REAL POWER (REAL GET (INT I) (INT I)) (INT I))))

Page 151: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

7.5. 練 習 問 題 151

>(entypeb ’(= a (power (getint i i) i)))

((REAL = (REAL A) (INT POWER (INT GETINT (INT I) (INT I)) (INT I))))

正しく割り当てられる場合には先のものと結果は変らないが,2番目の例では複数

の割当てがあることがよくわかる.以上はAda流の場合だが,言語によっては可能な

割り当てが複数あったときも,場合に応じてどれか 1つを選択するような仕様をもつ

ものもあり得る (特に組込み演算子と自動的な型変換に関連して複雑な規則が付随して

いる言語がある).そのような場合には単に誤りとする代りにそれぞれの場所で規則に

応じた選択を行う必要がある.

7.5 練 習 問 題

7-1. 自分の周囲に処理系がある言語をできるだけ多く集め,それらが型についてどの

ような立場を取っているか分類し,その違いは言語の使い勝手にどう影響するか

も考察せよ.例えば次のような分類基準を試せ.

• 型の概念があるかどうか.

• 翻訳時の型検査を前提としているかどうか.

• 型の同一性は名前による同一性か構造による同一性か.

7-2. 7.2節の単一化関数 unifyを改良して,単一化に失敗したときは*equiv-list*

を元に復元するようにせよ.次に,unifyを繰り返し呼び出して蓄えている全て

の型について互いに等しいかどうか調べる関数を追加せよ.

7-3. 名前による同一性を採用している言語のプログラムを 1つもってきて,その型定

義および型指定部分を取り出して問題 7-2のプログラムにかけ,「構造による同一

性を採用したとすれば互いに等しい」型がどれくらいあるか調べよ.もし S式表

現に直して打ち込むのが面倒なら,宣言部の抽象構文木を組み立てて S式表現で

打ち出すツールを yacc と lexでつくれば楽である.

7-4. 前章でつくった「簡単な宣言つき言語を処理する」プログラムを改良して,型定

義ができるように直せ.構文は好きなように決めてよいが,最低限配列とレコー

ドとポインタは含めること.

Page 152: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

152 第 7章 型  検  査

7-5. 問題 7-4のプログラムが扱う言語の構文を拡張し,式や代入先として配列要素,

レコードの欄,ポインタ参照を許すようにせよ.型検査を行うこと (ヒント: 全

ての節に型番号を入れる欄を増やしてしまうのが楽である).

7-6. 問題 7-5のプログラムに関数の宣言 (名前と引数や返値の型を教える)と呼出しを

加え,さらに多重定義を許すようにしてみよ.

Page 153: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

153

第8章 実 行 時 環 境

本書も道半ばにしてようやく解析部の話が終り,生成部の話に進むことができる.し

かし,コードを生成するに当たっては,まず実行時のプログラムやデータを主記憶上

にどう配置し,どのようにアクセスするかが決まっている必要がある.また場合によっ

ては,必要とされる機能を全て目的コードとして生成するよりあらかじめ用意した手

続き (実行時ライブラリ)を利用する方がよいかもしれない.本章ではこれらの話題に

ついて取り上げる.

8.1 実行時環境と名前の束縛

実行時環境とは,簡単にいえばコンパイラが生成した目的コードが実行されるときの

約束ごとである.具体的には,次のようなものが実行時環境の規約に含まれる.

• 記憶領域の配置や割り当て方

• 変数の種類ごとのアクセス方法

• 関数の呼び出し方/呼び出され方や引数の渡し方/渡され方

• 生成コードと実行時ライブラリの分担

これらの約束ごとを設ける主な目的は,ソースコードに現れてくる名前 (変数名,手続

き名,ラベル等)とそれらが表す実体 (記憶領域や命令列の特定の場所)の対応 (束縛,

binding)とその管理を効率よく実現することである.

例えばソースプログラム中の変数 xを主記憶上の特定の番地に割り当てたとすれば,

名前 xとその記憶場所の対応はプログラム実行開始時には確定し,実行中に変化する

ことはない (静的束縛 — static binding).静的束縛は実行時に記憶領域の管理に要す

るオーバヘッドがないという利点をもつ.一方,xが再帰呼出しを行う手続きの局所

Page 154: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

154 第 8章 実 行 時 環 境

変数である場合には,xに対応する領域をその手続きの呼出しごとに新たに割り当て

る必要がある.また,引数の受け渡しも実行時の束縛の変化を伴う.このように名前

と記憶場所の対応が実行時に定まる場合 (動的束縛 —dynamic binding)には,そのた

めの管理作業が必要である.

以下ではまず記憶域の種別と配置について述べ,スタックとそれに関連する引数や

返値の受け渡し,環境の切換えについて説明した後,ヒープの管理やごみ集めについ

ても触れる.

8.2 記憶領域の種別と割当て

プログラムが実行時に参照する記憶領域は,基本的には次のように分類できる.

• コード領域 — プログラムの命令を保持する.

• 定数領域 — 定数 (初期設定され,書き換えられないデータ)を保持する.

• 初期設定データ領域 — 初期設定され,書き換えられるデータの領域.

• 非初期設定データ領域 — 初期設定されないデータの領域.

• ヒープ領域 — 実行時に動的に割り当てられるデータの領域.

• スタック領域 — 局所変数など手続き呼出しに付随して割り当てられるデータ,

戻り番地,その他管理情報の領域.

これらの区分はオペレーティングシステムやハードウェアによってサポートされる場

合もあるし,処理系の中だけの規約として実現される場合もある.多くのシステムで

はハードウェアの機能を活用してコード領域や定数領域を書換え不可能なように保護

し,誤りによってプログラムが書き換わってしまうことを防ぐ.残りのデータ領域に

ついても,全てひとまとめに扱うシステムもあるが,スタックを特別扱いとし,スタッ

クあふれに対応して記憶領域を自動的に割り当てるシステムもある.

図 8.1に典型的な記憶域配置の例を示す.ここではスタックは上向きに伸びるように

描いたが,その配置や伸びる方向は通常ハードウェアやオペレーティングシステムに

よって自然な配置と向きが決まっている.実行時に動的に大きさが変化するのはスタッ

クとヒープであるので,この図のようにそれらを「向かい合せに」配置することが多

Page 155: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.2. 記憶領域の種別と割当て 155

コード領域

定数領域

データ領域(初期化)

データ領域(非初期化)

ヒープ領域

スタック領域

スタックが伸びる方向

ヒープを増やす方向

未使用の「穴」

 主記憶のアドレス空間

図 8.1: 典型的な実行時記憶域配置の例

い.実行開始時に主記憶領域の大きさを決定しておく必要のあるシステムでは,実行

中にヒープやスタックが不足しないように「穴」の大きさを決めることは簡単ではな

い.仮想記憶と大きな番地空間を提供するシステムでは番地空間上で「穴」を十分大

きく取り,必要になった時点で実際の記憶領域を割り当てることができる.

スタックとヒープ以外の領域については,大きさが実行時に変化することはないの

で,単に設計した配置に従って位置を割り当てるだけでよい.さらに,リンカを用い

る場合は目的コード中に「コード」「初期化データ」「非初期化データ」などの種別を

含めることができ,リンカが同種の領域をまとめながら詰め合せる.

また,記憶配置にはデータの種類に応じた境界揃え (boundary alignment)も考慮す

る必要がある.例えば多くのハードウェアでは 4バイト長の整数と実数は 4の倍数番

地,8バイト長の実数は 8の倍数番地に配置する必要がある.このため,変数に番地を

割り当てる際にはそこに入るデータの種類に応じてあきを挿入して番地を境界に揃え

る (または境界揃えを指定するアセンブラ用の指示を挿入する).レコードのように複

数のデータ型が混在する領域の場合にはその内部にも境界揃えのためのすきまが必要

かもしれない.スタックやヒープ上の領域でも同様の配慮が必要である.

Page 156: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

156 第 8章 実 行 時 環 境

8.3 スタックとスタックフレーム

8.3.1 スタックの用途

スタック (stack)とは,一番最後に割り当てられた領域が一番先に解放される (LIFO

— last in, first out)ような割当てを実現するデータ構造である.プログラミング言語

における手続き呼出しでは,一番最近に呼び出された手続きが一番最初に戻るので,手

続きに局所的な記憶領域 (局所変数や一時変数)はスタックに割り当てるのが自然であ

る.これを含めて,手続き呼出しに付随する情報としては次のものがあげられる.

(a) 手続きに局所的な作業領域

(b) 戻り番地の情報

(c) 引数の情報/戻り値の情報

(d) 呼出し元の領域を示す情報

(e) 外側のスコープを示す情報

(f) 呼びに伴って壊れると困るレジスタ内容の写し

多くの手続き型言語の実現ではスタックを 1本ですませるが,Prologなどでは呼出し

と領域割当て/解放が同期しないため呼出しスタックとデータ領域スタックを分ける必

要がある.また Lispのようにごみ集めを必要とする場合は一般のデータを割り当てる

スタック (データスタック)と手続きの呼び/戻り情報を積むスタック (制御スタック)

を分けることもある.以下では 1本のスタックを用いた実現について述べる.

8.3.2 スタックフレームとフレームポインタ

1本のスタックを用いる実現では前節 (a)~(f)の情報を各手続き呼出しごとに 1組に

まとめてスタック上に割り当てる.これをスタックフレーム (stack frame)ないし活性

レコード (activation record)とよぶ.典型的なスタックフレームの形を図 8.2に示す.

フレームの大きさは,局所変数に大きさが実行時に変化するもの (可変長配列など)を

含まない限り,手続きごとに翻訳時に決まる.引数の受け渡しと環境の切換えについ

ては次節以降で説明するので,以下ではそれら以外の部分について説明する.

Page 157: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.3. スタックとスタックフレーム 157

戻り番地

引数領域

局所変数

作業領域スタックの先頭

スタックが伸びる向き 一つの手続き呼出しの

スタックフレーム

図 8.2: 典型的なスタックフレームの例

まず手続きが呼び出された時点でのスタックの状態について考える.多くの命令セッ

トアーキテクチャでは,スタックの先頭を指すレジスタ (スタックポインタ — stack

pointer,SP)が決められている 1.手続き呼出し命令 (ないし呼出し規約)によって,戻

り番地がスタックの上に積まれる.この様子を図 8.3(a)に示す.

戻り番地SP

引数領域

戻り番地

SP

引数領域

FP 旧FP

変数x

変数y

(a) 呼ばれた直後 (b) FPの設定と局所変数割り当て後

図 8.3: フレームポインタ

次にこの上に局所変数の領域を確保するが,それにはスタックポインタを必要なだ1ハードウェアとしては決まっていないが,オペレーティングシステムの規約としてスタックポインタに

使うレジスタを規定する場合もある.

Page 158: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

158 第 8章 実 行 時 環 境

けずらせばよい.ただし,スタックは無限にあるわけではないので,領域を確保する

際にスタック領域に十分なあきがあるかどうか検査する必要がある 2.スタック領域を

自動管理してくれるオペレーティングシステムの場合にはあらかじめ決められた大き

さまでスタックは自動拡張され,それを越えるとシステムがその旨通知してくれるの

で検査は不要である.

スタック上に確保した領域のアクセスは,この領域の番地が手続き呼出しごとに変

わるため,固定番地による指定では行えない.ほとんどの計算機ではこのために「あ

るレジスタの指す場所から nバイト先」という番地指定を許す.そこで,局所変数領

域の先頭を指すレジスタ (スタックフレームの起点を指すことからフレームポインタ

— frame pointer,FP)を用いる.この様子を図 8.3 (b)に示す.例えばある手続きで

大きさ 4バイトの局所変数 xを起点の 4バイト先,yを 8バイト先に割り付けたとす

れば,yの値を xに代入するには

movl fp@(-4),fp@(-8)

というコードを出せばよい 3.

しかし,フレームポインタは別の手続きを呼ぶとそこでも同様に利用するので,戻っ

て来たときには別の値に書き変っている.そこで,どの手続きでも呼ばれたらまずフ

レームポインタをスタックに格納し,その場所をフレームの起点だと考えてフレーム

ポインタにはその番地を入れるものとする.手続きから戻るときには起点に入ってい

る旧フレームポインタ値をフレームポインタに入れ直すことでもとの値が復旧できる.

このようにすると,フレームポインタは常に 1つ前のスタックフレームのフレーム

ポインタを保存した番地を指しているので,フレームポインタから始まる連鎖をたど

ることでスタック上のフレームを新しいものから順にたどることができる.これを利

用して,誤りや実行中断点 (ブレークポイント)への到達によって停止したプログラム

の状況を調べ,その時点で各手続きの局所変数などを調べるデバッガをつくることが

できる.

コード上の各場所でスタックポインタとフレームポインタがどれくらい離れている

かは,局所変数として大きさが実行時に変化する領域を割り当てない限りは,翻訳時

2呼出しごとに検査を行うのはかなりのオーバヘッドとなるので,一切検査を省略してプログラマの見積りに任せる処理系もあるが,少なくとも虫取り時には検査が行えることが望ましい.

3以下では説明のため Sun-3(CPU は MC68020) 用の UNIX アセンブリ言語形式を使用する.ここでmovlは 1語の値をコピーする命令,Rx@(n)はレジスタ Rxの指している番地に nを加えた場所を意味する.

Page 159: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.3. スタックとスタックフレーム 159

にわかる.したがって,スタックポインタとフレームポインタという 2つのレジスタ

をこのために割り当て管理するのは無駄であり,どちらか 1つだけですませることも

可能である (可変長配列などを割り当てる手続き内では特別に両方使うようにすればよ

い).実際,そのようなコンパイラも存在する.ただし,その場合にはデバッガなどの

ために「どの番地を走っているときはスタックポインタとフレームポインタはどれだ

け離れているか」の情報を別に用意して参照させる必要がある.ここでは当面簡単さ

を優先させて,2つのレジスタを使用することを前提としておく.

8.3.3 引数と返値の受け渡し

引数は呼ぶ手続きから呼ばれる手続きに渡されるため,両方の手続きからアクセスさ

れる.言い換えれば,スタックフレームはある手続きに固有の環境を表すが,例外とし

て引数だけは隣接フレームと共有される.なお,関数の返値も手続き間で受け渡され

るが,戻りによって呼ばれ側のスタックフレームは消滅するので共有は起きない.本

項では引数の各種受け渡し機構および返値の渡し方について述べる.以下では引数を

呼び側から見る場合と呼ばれ側から見る場合で区別するため,前者を実引数,後者を

仮引数と記す.

a. 値呼び  値呼び (call by value)は最も単純かつ基本的な受け渡し機構であり,

呼び側では任意の式の値を実引数として渡す.呼ばれ側では仮引数は渡された値を初

期値としてもつような局所変数として扱う (ただし Pascalなどでは値渡し引数への代

入を許さない).値呼びを実現するには,各実引数の値を計算し,それを順にスタック

上に積むだけでよい.例えば sub(100, -10)という呼出しの場合,図 8.4(a)のよう

に,100と-10をスタックに積んでそのまま subを呼び出す.subの方では仮引数を

a,bという名前でアクセスするとすれば,それらは (旧フレームポインタも戻り番地

も 4バイトの領域であるとすれば)フレームポインタ起点で 8バイトおよび 12バイト

手前 (大きい番地)にあることになる.そこで,例えば a + bという式であれば

movl fp@(8),d0

addl fp@(12),d0

のように他の局所引数と同等の命令でアクセスすることができる.なお,この例では

スタック上に実引数を右から順に積んでいるが,こうしておくと引数の数が可変で,実

Page 160: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

160 第 8章 実 行 時 環 境

(a) (b) 呼ばれ側

戻り番地

局所変数領域

100

-10呼び側のフレーム

100

-10

局所変数領域呼ばれ側の

フレーム

積む

FP

FP起点でアクセス

旧FP

SP

呼び側

図 8.4: 値呼びの実現

際に渡した数は第 1引数を調べるとわかるような関数 (Cの printfなど)が素直に実

現できる (第 1引数の位置は常に fp@(8)で,残る引数はそれに隣接するから).可変

引数を使わなかったり,実引数の数を別の方法で受け渡すなら左から積むのでもかま

わない.

b. 参照呼び  値呼びは単純でわかりやすいが,呼ばれ側から呼び側に情報を渡

すうえでは不便である.これに対し,仮引数に対する更新がただちに実引数にも及ぶ

ような呼出し機構が参照呼び (call by reference)である.参照呼びでは図 8.5(a)のよ

うに,スタックには実引数の入った場所の番地を積んで渡す.そして,呼ばれ側では

この番地情報を読み出して実引数にアクセスする.例えば上の例で呼ばれ側において

b = a + bを実行するコードは次のようになる 4.

movl fp@(8)@,d0

addl fp@(12)@,d0

movl d0,fp@(12)@

参照呼びにおいて実引数が定数の場合,その定数 (例えば-10)が呼ばれ側で更新さ

れてしまうのでは困る.この問題に対しては,Pascal のように参照呼びの実引数には

変数しか書けないという言語仕様で対処することもあるが,そうでなければ定数や式

を渡すときには値を適当な作業領域に格納してその番地を渡す必要がある.

4Rx@(n)@と書いた場合,レジスタ Rx が指している番地の n 番地先に入っているものを番地とみなし,その番地にアクセスする.

Page 161: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.3. スタックとスタックフレーム 161

(a) 呼び側 (b) 呼ばれ側

戻り番地

100

90

呼び側のフレーム

局所変数領域呼ばれ側の

フレームFP旧FP

SP

100

-10

図 8.5: 参照呼びの実現

c. 名前呼び  参照呼びのような引数渡し機構を採用する動機の 1つは前述の

ように「呼ばれた側から読んだ側への情報伝達」であるが,もう 1つの考え方として,

「手続きを,その呼出し箇所に手続き本体のコードを (適切な名前置換えの後)埋め込

むものとして理解する」という立場があり得る.もしそのように考えることを許すな

ら,例えば

procedure sums(int k, a, b; real x, result)

begin

result := 0.0;

for k := a to b do result := result + x;

end;

のような手続きがあったとして,これを

sums(i, 1, 10, a[i,i], r);

のように呼び出すと

r := 0.0;

for i := 1 to 10 do r := r + a[i,i];

と書いたのに等しいから,したがって大きさ 10の行列 aの対角要素の和が求まるはず

である.しかし,実際には参照呼びの場合,仮引数 xに相当する番地を呼出し時に計

算して以後それを用いるため,上記のようなことはできない.これを可能にするには

Page 162: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

162 第 8章 実 行 時 環 境

名前呼び (call by name)とよばれる引数渡し機構が必要である.名前呼びは Algol-60

言語で最初に採用された.

名前呼びでは,呼ばれた側で仮引数が参照されるたびに,その仮引数のありかや値

を計算し直す必要がある.そのため,参照呼びのように実引数の番地を渡す代りに,実

引数の番地や値を計算する手続きを渡す.この手続きを伝統的にサンク (thunk)とよ

ぶ.実現方法にもよるが,サンクは各引数ごとにその値が参照されたとき (右辺値)用

とその場所に代入するとき (左辺値)用の対で用意することが自然である.

例えば上の例だと,k,resに対応するサンクは呼ばれると常に i,rの値 (右辺値

用)と場所 (左辺値用)を返す.また a,bに対応するサンクは右辺値用のみで,常に 1

や 10を返す.一方,xに対応するサンクは呼ばれるごとにそのときの iの値に応じて

適切な a[i,i]の値や場所を計算して返す.したがって,このサンクは変数 iを参照で

きなければならない.そしてもし呼ばれた手続きが別の iという変数をもっていたと

しても,間違ってそれを参照してはならない.これを実現するには,次節で述べる環

境の切換えを正しく行う必要がある.

名前呼びの実現は複雑であり,効率上も不利であるので,最近の言語での採用例は

ほとんどない.ただし,マクロ機構 (構文上は手続き呼出しに見えるが,その部分を字

面上で定義本体により置き換えたうえで翻訳する機構)を用いる場合には,起きること

は名前呼びと等価である.ただしマクロ機構ではその場に本体を展開してしまうので,

呼出し機構もサンクも不要である.逆に,参照呼びや値呼びの言語で実行効率向上の

ためにその場展開を行う場合には,これらの呼出し機構と名前呼びとで結果が異なる

場合に留意する必要がある.

d. 複写復元呼び  呼ばれ側から引数を通じて呼び側に情報を返したいが,番地

の間接参照が遅いなどの理由で参照呼びにしたくないときしばしば代りに使われるの

が複写復元呼び (copy-restore linkage)である.この方式では図 8.6に示すように,値

呼びと同様に値を渡してしまい,呼ばれ側でその場所を参照/更新し,戻り時にはその

場所から対応する実引数の場所に値をコピーし戻すことで呼び側に情報を返す.複写

復元呼びと参照呼びは常に同じ結果をもたらすとは限らない.例えば

procedure sub(integer i, j) begin i := i + 1; j := j + 1 end;

のような手続きが次のように呼ばれる場合を考えてみる.

x := 10; sub(x, x);

Page 163: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.3. スタックとスタックフレーム 163

x

y

100

-10

x

y

100

-10

x

y

FP FP

FP

100

-10

350

200

350

200

350

200

呼ぶとき:値を積む 呼ばれ側:値を更新 戻ったとき:値を復元

参照・更新

図 8.6: 複写復元呼び

参照呼びでは subの中で xが 2回増やされるので戻ってきたときの xの値は 12にな

る.しかし複写復元呼びでは xの値 10 が複写されて渡され,それらが別個に増やされ

て 11になり,戻りのときに xの場所に重ね書きで復元されるので xは 11になる.言

語によってはこの種の (引数渡し機構によって動作が変化する)コードを禁止し,引数

渡しを参照呼びと複写復元呼びのどちらで実現してもよいものもある.

e. レジスタによる受け渡し  ここまででは引数は受け渡し機構の如何によらず,

全てスタック上に積まれて受け渡されるものとして扱ってきた.しかし,毎回全引数

をスタックに積み (すなわち主記憶上のどこかに書き込み),呼ばれ側でまたレジスタ

に読み出すのは無駄である.そこで,引数の一部または全部をレジスタに入れたまま

渡す工夫も多く行われる.その際は,どんな場合どの引数をどのレジスタで渡すかに

ついて,例えば「1引数の関数で,その引数がレジスタに載るようなデータ型である場

合のみレジスタ 0番に入れて渡す」「全ての手続きにおいて,最初の 3引数まではレジ

スタ r1~r3で渡すが,ただしレジスタに入り切らないデータの場合には代りに番地を

渡す」のような規約が必要である.

f. 返値の受け渡し  返値についても,基本的にはスタック上で渡すことがきる.

ただし,図 8.2のように引数領域のすぐ上に戻り番地が積まれていると返値を入れる

場所がない.そこで 呼び側であらかじめ返値を入れる場所を (たとえば戻り番地のす

Page 164: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

164 第 8章 実 行 時 環 境

ぐ下などに)確保するなどの工夫が必要である.

簡単でよく使われ,効率がよいやり方は特定のレジスタに返値を入れて戻ることで

あるが,レジスタに入らない大きさのデータの場合が問題になる.対策としては,その

ような例外的な場合のみ呼び側で領域を確保したり,どこか適当な場所 (たぶんスタッ

クの上の方)に返値を置き,その番地をレジスタに入れて返すなどがある (その場合に

は呼び側がただちに返値を適切な場所にコピーする必要がある).

8.3.4 レジスタの退避回復

レジスタはデータのアクセスや演算のために多様に使われるが,ある手続きから別の

手続きを呼んだときには,その呼ばれ側でも同じようにレジスタを使用するだろうか

ら,呼ぶ前と戻ってきた後では各レジスタの値は違っているかもしれない.レジスタを

式の途中結果や引数/返値の受け渡しのみに使用するのであればあまり問題はない (式

の評価の途中で関数を呼んだりするときは注意が必要である).しかし,効率よいコー

ドのためにはよく参照される使われる値をできるだけ長くレジスタに置いて主記憶ア

クセスを避ける必要がある.その場合,それらのレジスタの値が手続き呼出しによっ

て変わってしまうのは不都合である.これに対処するやり方としては,次の 2つの方

法がある.

• 呼び側での保存 (caller-save) — 手続きを呼ぶ側で,内容を壊されては困るレジ

スタの内容を保存してから呼び,返って来たらその内容を復元する.

• 呼ばれ側での保存 (callee-save) — 呼ばれた手続きの側で,呼ばれた直後に自分

が内容を壊すレジスタを保存し,戻る直前に復元する.

どちらにも固有の利点と欠点がある.前者は,実際に壊されると困るレジスタのみを

保存できるが,手続き呼出しが多数あると保存/復元用コードが多量に生成される.後

者は,実際には使っていないレジスタを保存してしまうかもしれないが,保存/復元の

コードは手続きの入口と出口に 1箇所ずつですむ.いずれにせよ,1つの言語処理系

ではどの方法を採用し,具体的にどのレジスタを退避回復するかを統一する必要があ

る.上記の特徴を考慮して,数個のレジスタは呼び側での保存,他の数個は呼ばれ側

での保存とした処理系もある.

Page 165: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.4. 環境の切換え 165

呼び側での保存では,保存領域はフレーム内のあらかじめ決まった位置に配置でき

る.または引数を全て積んだ後のスタック上に積むのでもよいが,その場合は保存す

るレジスタ数が変ると呼ばれ側のフレーム起点から引数までの距離が変ってしまうの

でこれに対処する必要がある.例えば,保存する個数に関わらず決まった大きさの領

域を取る,引数領域の番地も別にレジスタで渡す,などの対応がある.

呼ばれ側での保存では,戻り番地 (=旧プログラムカウンタレジスタ)や旧フレーム

ポインタの領域に隣接して (つまり局所変数より手前に)レジスタ保存領域を取るのが

普通である.実際,これらの保存をまとめて行う命令を備えた計算機もある.

8.4 環境の切換え

8.4.1 静 的 チェイ ン

ここまででは,手続きからはその手続きの局所変数と引数のみを参照するものとして話

を進めてきたが,実際にはこれら以外の変数も存在する.まず広域変数や静的変数に

ついては,初期設定データ領域や非初期設定データ領域に割り当てられ,実行時には

固定した番地をもつので,固定番地を参照する命令を使用すれば特に問題はない.ブ

ロック構造の言語ではさらに,手続きの中に手続きが入れ子になっている場合に内側

の手続きから外側の手続きの変数を参照できる.例えば次のような具合である.

procedure p;

var x: integer;

procedure q; begin x := 1 end;

begin q end;

この場合,手続き qの中で x := 1を実行するためには pのフレームの中の xにア

クセスできる必要がある.一見これは図 8.7(a)のように,フレームポインタから xま

での距離が一定なので簡単そうに見える…が,次の例および (b)のように別の手続き

を介して qが呼ばれる場合もあるので簡単ではない.

procedure p;

var x: integer;

procedure q; begin x := 1 end;

procedure r; begin q end;

begin q; r end;

Page 166: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

166 第 8章 実 行 時 環 境

戻り番地FP

戻り番地

x

FP

戻り番地

x

q

p

q

p

戻り番地

r

(a) → (b)

戻り番地

P Q →P Q→R

図 8.7: 外側の環境へのアクセス

xを正しく参照するには pのフレームポインタを覚えておき,その値を起点として

用いる必要がある.そのため,図 8.8のように,旧フレームポインタを入れておく場

所のすぐ上に外側の環境 (qや rでは pのフレームの起点) へのポインタを入れておく.

qから pより外側の中間レベルの変数へのアクセスは,まず qから p のフレームをた

どったあとで,pのフレームに格納されている外側の環境 (フレーム)へのポインタを

さらにたどって行けばよい.なお,この外側の環境への連鎖のことを (翻訳時に静的に

定まる手続きの入れ子関係に対応しているため)静的チェインとよび,これと対比して

フレームポインタの連鎖を動的チェインとよぶこともある.静的チェインを管理する

には,手続き呼出し時に引数に加えて呼び側から呼ばれ側に外側の環境の値を渡せば

よい.渡される値は呼ぶ側と呼ばれる側の関係に応じて次のようになる.

• 自分と同じレベルの手続きを呼ぶとき (先の例では r→q) — 自分の 1つ外側の

環境 (=自分の静的チェインの値) を渡す.

• 自分の 1レベル内側の手続きを呼ぶとき (先の例では p→q) — 自分の環境その

もの (=現在のフレームポインタの値)を渡す.

• 自分の nレベル外側の手続きを呼ぶとき (先の例で qや rから pを呼ぶとき) —

nレベル外側の環境を渡すので,静的チェインを n回たどってその値を渡す.

Page 167: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.4. 環境の切換え 167

FP

戻り番地

x

q

p

戻り番地

r

戻り番地旧FP

旧FP

旧FP

図 8.8: 外側の環境へのアクセス (2)

8.4.2 ディス プ レ イ

静的チェインを用いて n個外側の環境を参照するには,その環境を求めるため n段チェ

インをたどる必要があり,時間がかかる.これを改善する 1つの方法は,各手続きが呼

ばれるごとに,それが nレベルのものであれば n− 1 . . . 1レベルの環境の値を手近な

ところに保管しておくことである.これをディスプレイ (display)とよぶ.ディスプレ

イを実装する場合,各ディスプレイ値は静的チェインの各レベルの値と同じだが,毎

回静的チェインからディスプレイをつくるのでは非効率的なので次のように呼出しご

とにディスプレイ一式を更新していくのが普通である.

• 自分と同レベルの手続きを呼ぶ場合— ディスプレイは自分のものと同じでよい.

• 自分より 1段内側のレベルの手続きを呼ぶ場合— 現在のディスプレイに,自分

のフレームポインタの値を追加したものを渡す.

• 自分より n段外側のレベルの手続きを呼ぶ場合 — ディスプレイの内側から n個

を取り除いたものを渡す.

ディスプレイの置き場所はスタックフレーム中の旧フレームポインタに隣接させるこ

とが多いが,レジスタ数が十分多い場合には (全部または最内側の数個を)レジスタに

保管することでさらに参照を速くできる.実際,そのための専用レジスタをもつハー

ドウェアも存在する.ただし現実には,ブロック構造の言語において中間レベルの変

Page 168: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

168 第 8章 実 行 時 環 境

x

旧FP

旧FP

旧FP

q

s

p

図 8.9: 手続き引数と環境

数をアクセスすることは (プログラミングスタイルにもよるが)多くはない.したがっ

て,呼出しのたびに一群のディスプレイを管理する手間を考えると,ディスプレイを

使うことが常に得だとは必ずしも言えない.

8.4.3 手 続 き 引 数

ここまではプログラム中に直接呼び出される手続きが書かれている場合を扱って来た.

しかし,手続きの引数として別の手続きを渡す場合にも,これまでに記したと同様環

境チェインやディスプレイを正しく管理する必要がある.例えば次の例を考える.

procedure s(px: procedure); begin px; px end;

procedure p;

var x: integer;

procedure q; begin x := x + 1 end;

begin x := 0; s(q) end;

pは sに手続き qを渡し,sの中では渡された手続きを 2回呼ぶので,pの終りでは

xは 2になっているはずである.静的チェインによる実現を考えると,qが呼ばれてい

るときのスタックの様子は図 8.9のようになっていなければならない.これが正しく

実行可能であるためには,sから qを呼ぶときに pの環境ポインタを渡す必要がある.

すなわち,手続き引数を渡す際には手続きそのものの番地に加え,その手続きが呼ば

れる際の環境の値も一緒に渡せばよい.環境ポインタの計算方法は 8.4.1項で述べたの

Page 169: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.5. ヒープとその管理 169

使用中

未使用

先頭

末尾

x

使用中

未使用

x+n

割り当てた領域

図 8.10: 基本的なヒープの実現

と同じである.ディスプレイによる実現の場合にも,同様に直接呼ぶ場合と同じやり

方で新しいディスプレイを計算し,手続き引数にその情報を含める.そして,手続き

引数を呼び出す際にはこのディスプレイを用いて呼び出しを実行すればよい.

8.5 ヒープとその管理

8.5.1 ヒープの位置付けと機能

前節までに見たように,手続きの呼びと戻りに同期して記憶領域が必要な場合はその

領域をスタック上に取ることで効率良く管理できる.一方,手続きの呼びや戻りと無

関係に割り当られる動的記憶領域はヒープ (heap)を必要とする.ヒープの機能は基本

的には「nバイトの領域を割り当ててその先頭番地を返す」ことであり,そのごく基

本的な実現方法は例えば図 8.10のようになる.すなわち,ヒープの先頭,大きさ (ま

たは末尾),および未使用部分の先頭を覚えておき,nバイトの領域を要求されたとき

は未使用部分の先頭を nバイト進め,これまでの未使用部分の先頭を返せばよい.ご

く単純であるが,現実には以下の点にも配慮する必要がある.

まず,ヒープに取られるデータについても割り当てる領域の境界揃えは必要であり,

そのためにいくつかの方法がある.より望ましいのは領域割当てに際して大きさに加

えて境界揃え条件も指定することで,特にコンパイラがヒープ割当て命令 (ないし手続

き呼出し)を生成する場合には,コンパイラに境界揃え条件を出力させるのは自然なや

Page 170: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

170 第 8章 実 行 時 環 境

り方である.そのような方法が取れない場合には,常にハードウェアが要求する最大

の境界揃えを行うか (領域の無駄が多くなる可能性がある),または領域の大きさから

境界揃え条件を推定することになる.

また,ヒープ領域を使い尽くしたときの処置も考慮する必要がある.一番簡単なの

は実行をエラー終了させることであるが,オペレーティングシステムに頼んで実行領

域を増加させられるならそうした方がよい.その場合,新たに割り当てられたヒープ

はこれまでのヒープに隣接していないかもしれない.ところで,一度割り当てた領域

を最後までずっと使うというプログラムは必ずしも多くなく,したがっていったん割

り当てたが後に不要になった領域を回復 (reclimation)することが有効である.その場

合,繰り返し回復を試みてもあき領域が確保できないときはじめてヒープを増加させ

るようにする.

8.5.2 手動による領域回復

領域回復を行う 1つの方策として,プログラマが明示的に不要になった領域を返却す

る旨記述する,という方法がある.これはさらに,領域をまとめて返却する方法と個

別に返却する方法に分けられる.まとめて返却する,というのは例えば図 8.10の割付

けにおける「未使用領域の先頭」を覚えておき,領域が不要になった時点でその位置

に先頭を「巻き戻す」というものである.例えばコンパイラの場合を考えると,1つの

翻訳単位を翻訳するために割り当てたデータ領域をその翻訳単位の処理が終った時点

でまとめて解放するというのは十分考慮に値する.

しかしこの方式は柔軟性に乏しいのも確かであり,手動による回復といった場合普

通は割り当てた領域単位で返却できるようにするのが一般的である.そのような領域

管理は例えば図 8.11のように実現できる.まず,空いていて割当て可能な領域を単リ

ストの形で連鎖させて保持する.各領域の先頭 (ヘッダともいう)には次の領域へのポ

インタとこの領域の大きさを入れておく.例えばヘッダに 8バイト必要だと仮定する.

xバイトの領域を要求されたときは s = x+ 8として,sバイト以上の大きさの領域を

単リストから探し,見つかったらそこから sバイト切り取り,その先頭に大きさ sを

格納したうえで 8バイト先を要求された領域の先頭として渡す.もし見つかった領域

が切り取る余地のないほどぴったりだったら,切りとる代りにその領域を単リストか

Page 171: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.5. ヒープとその管理 171

n

m

使用中

n bytes

m bytes

n-s

m

使用中

sここの番地を渡す

n-s

m

使用中

s

先頭 先頭 先頭

割当て 返却

図 8.11: 返却可能なヒープの実現

ら外して同様に 8バイト先を渡す.返却されたらそれを単リストの先頭につけ加えて

次の割り当てに備える.

この方法は簡潔だが,領域がだんだん細切れになり,その後で大きい領域を要求され

ると全体としては足りるのにまとまった領域がなくて割り当てに失敗するという弱点

がある.このような現象を断片化 (fragmentation)とよぶ.このため,上述のように割

当て可能な最初の領域が見つかったら割り当ててしまう (first fit)代りに一番ぴったり

した領域を探したうえで割り当てる (best fit)方法も考案されたが,経験的にはかえっ

て細切れが増えてしまうとされている.

断片化を減らす 1つの方法として,見つかった領域と要求された領域の大きさがあ

る程度近ければ切りとらずにそっくり渡すというものもある.もう 1つの対策として,

返却された領域が別のあき領域に隣接している場合はそれを併合して 1つの領域にす

る方法もある (図 8.11 の中央から左に戻る).この場合は単リストを領域の先頭番地の

昇順に並べておく (そうしないと,返却された領域が上下ともに空き領域に隣接してい

るときに 3つの領域を 1つに併合するのが面倒である).

手動による領域回復では要らなくなった領域をきちんと区別するのが繁雑であり,使

用中の領域を誤って返却するという誤りが避けられない.そのような誤りは,その領

域が再び割り当てられて使われるまで何事もなく見えるため非常に発見しにくい.逆

に使わなくなった領域を返却し損なうという誤り (メモリの漏洩 — memory leak) も

起こるが,こちらは徐々に主記憶が圧迫されてくるというだけで,あまり致命的では

Page 172: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

172 第 8章 実 行 時 環 境

ない (そのためそれとわからず見過ごされたままでいることも多い).

8.5.3 ご み 集 め

手動による領域回復にまつわる問題点を解決する 1 つの方法はごみ集め (automatic

garbage collection)を採用することであり,設計時点の新しい言語は多くがそうなって

いる.ごみ集めの基本的原理は次のようなものである.ヒープ上の領域はその番地が

どこか別の領域に格納されていない限り参照できない.「別の領域」というのは図 8.12

のように,スタック上の変数や広域変数のこともあるし,参照できるとわかっている

ヒープ上の別の領域のこともある.この参照できる領域を「生きている」という.そ

して「生きていない」領域 (この図では xと y)は「ごみ」であり,ごみを回収して再

利用することがごみ集めの目的である.以下ではごみ集めを行う処理系に課せられる

条件およびごみ集めの諸方式について解説する.

スタック ヒープ

x

y

図 8.12: ごみ集めの原理

a. ごみ集めのための条件  ごみ集めを行うには,言語処理系の実行時環境が次

の条件を満たす必要がある.

1. ごみ集め機構が,ヒープ外にある全てのポインタ値 (これを根ないしルートとよ

ぶ)にアクセスできること.

2. ヒープ内の個々の領域を順番に調べていくことができること.

3. ヒープの内外に関わらず,ポインタ値とそれ以外の値を区別できること.

Page 173: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.5. ヒープとその管理 173

ただしごみ集め方式によってはこのうちのいくつかは緩和され,また別の条件が必要

となる場合もある.まず条件 1については,「根から直接的,間接的にアクセスできる

領域」を知るために必要である.そして,それ「以外の」領域をごみとして回収する

ためには条件 2が必要なはずである.この条件は図 8.11のような領域管理をすれば自

然に満足される.

最後に,条件 3は「全ての根から始めて順次ポインタをたどって行く」ことではじめ

てどれが生きている領域かわかるため必要になる.この条件が言語処理系を実現する

うえで最も厄介である.ごみ集めを前提としない言語処理系ではポインタも整数も内

部的には同一表現になるのが普通であるが,そうするとデータを見ただけではポイン

タかどうかはわからない.そこで全てのデータにタグ (tag,印)のビットを割り当て,

そこを見ればポインタかどうかわかるようにする.しかし,32ビットのうち 1ビット

をタグにしてしまうと,整数型の値の範囲がそれだけ狭くなり,他の言語のコードと

連絡するときなどに問題が生じる.他方,32ビットとは別にタグを取ろうとすると,

データが一度にレジスタに載らないため実行速度が遅くなるなどの弊害がある.

この問題に対する絶対的な解答は存在しないが,例えばタグつきの値として表せる

整数の範囲は狭くとどめ,それより大きな整数はヒープ上の領域内のみに置くような

処理系が多い.ヒープ上の領域については,「ここからここまでがポインタ値」という

情報をヘッダに含めその部分だけをたどることで安全に任意のビットパターンを格納

させられる (大きな整数の操作は遅くなるが,そのような大きな整数はあまり現れない

という前提に基づいている).

b. マークスイープ型ごみ集め  マークスイープ (mark-sweep)型ごみ集めは最

も基本的なごみ集め方式であり,各領域にごみか否かを表す印をもたせ,以下の手順

によりごみ集めを行う.

1. 全ての領域に「ごみである」という印をつける.

2. 根から始めて全ての生きている領域をたどりながら,たどったものについては

「ごみである」という印を消す.

3. 再び全ての領域を順に調べて,「ごみである」という印がついたままのものを回収

する.

実現上は効率のために 3と次回のための 1を同時に行うので,結果的に「たどって印を

消す」(mark)フェーズと「順に調べていく」(sweep)フェーズに分けられることから,

Page 174: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

174 第 8章 実 行 時 環 境

この名がある.原理的には単純でわかりやすいが,このままだと一度割り当てた領域

の番地は決して移動しないため,次第に領域が細切れになる.これを補うため,時々

詰合せ (compaction)フェーズを実行するようにもできる.詰合せのためにはヘッダに

「この領域はどの番地へ移動する」という情報も含める必要がある.手順は次の通り.

1. ごみでないオブジェクト全てについて,「ごみは取り除いて領域の頭から詰め合せ

たとしたらこの領域は何番地になるか」をヘッダに記録する.

2. 全てのポインタを,新しい番地を指すように修正する.これは修正する前のポイ

ンタをたどってオブジェクトのヘッダにある情報を見ればできる.

3. オブジェクトを実際に移動して詰め合せる.

詰合せを行うと,生きている領域がヒープの先頭にまとまるので,仮想記憶システム

の場合にはページフォルト (参照ページがディスクにありオペレーティングシステムの

介在を必要とすること)の頻度が下がってプログラムの実行速度が向上するという効果

もある.

c. 複写型ごみ集め  複写型ごみ集めの原理は次の通りである.まずヒープを 2

つの領域に等分し,片方からのみ領域を割り当てる.その最後まで来たらごみ集めを

起動し,根からたどって生きているオブジェクトを全て他方の領域にコピーするとと

もにポインタも新しい方を指すようにつけ替える.全部コピーし終ったら残っている

のはごみだけだから,2つの領域の役割を入れ替える.

原理は以上だが,共有のあるデータ構造を正しくコピーするためには前述の詰合せ

と同様,各オブジェクトのヘッダに「この領域はどの番地にコピーされた」という情

報を入れる場所を用意する.最初はこれを nilにしておき,コピー時にコピー先の番地

を書き込む.たどりながらコピーしている途中でここが nilでない領域が見つかった

ら,それはコピーせずにすでにコピーしたものを指すようにする.例えば図 8.13では

まず根 aからたどってオブジェクト群をコピーした後根 bから始まるオブジェクト群

をコピーし始めるが,領域 xをコピーした後そこから指されている領域 yについては

コピーせず先にコピーした y’ を指させる.

複写型ごみ集めは詰合せが自然に行えるうえ,「ポインタでつながったものどうしは

近くにコピーされる」というより望ましい性質をもつが,一方で毎回全データをコピー

するためオーバヘッドが大きいこと,ヒープ領域が常に半分しか使われないことなど

の弱点をもつ.これに対し,コピーの手間を減らすためにオブジェクトを複数の世代

Page 175: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.5. ヒープとその管理 175

スタック

ヒープ1

ヒープ2

b

a

x

y

y’

図 8.13: 複写型ごみ集め

に分割して,一部の世代だけに限定してごみ集めをする改良が知られている.領域の

大きさについては,仮想記憶システムではアドレス空間は消費されるがごみ集め以外

の使用は半分ずつになるのであまり問題ではないとされている.

d. 参照計数型ごみ集め  ここまでに説明したごみ集め方式ではプログラムの実

行フェーズとごみ集めフェーズが明確に分かれていて,ごみ集め中は通常のプログラム

実行が中断させられるという弱点がある.これは特に対話性や実時間性を要求される

プログラムで問題になることがある.この問題を緩和するためにマークスイープ型や

複写型のごみ集めを「小刻みに」行う方法もあるが,原理的に実行とごみ集めのフェー

ズ分けがない漸進型 (incremental)ごみ集めを採用することも 1つの方法である.参照

計数 (reference counting) 法は漸進型ごみ集めの代表的なものであり,その原理は次の

ようなものである.

1. 全領域のヘッダにその領域を指しているポインタ数を記憶する場所を設ける.

2. ポインタ値の複写や書換えが起こるつどこの計数値を正しく更新する.

3. 計数値が 0になったときはこの領域への参照は残っていないので,領域をごみと

して回収する.

例えば,図 8.14のような状況でこれまで xの中に保持されていた zへのポインタを y

Page 176: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

176 第 8章 実 行 時 環 境

へのポインタに書き換えると,yの計数は 1増え,z の計数は 1減ることになる.その

結果 zの計数が 0になるので,zはごみとして回収できる.参照計数の問題点は,全て

スタック

ヒープ

x

y

1

1

1

1

2

1 0z

11

図 8.14: 参照計数型ごみ集め

のポインタ書換えに際して計数を更新するオーバヘッドが大きいという点と,図 8.14

の下にあるような「環状のごみ」は計数が 0にならないため回収できないという点で

ある.前者の問題についてはスタック上の変数の書換えに対して更新を先送りしてま

とめて実施することでオーバヘッドを減らすような改良が知られている.環状のごみ

に対しては,「輪は不要になったら切り開いておく」ようにプログラマが気をつけると

ともに,ときどきマークスイープなど別のごみ集めを併用することが一般的である.

e. ごみ集め手法の改良と組合せ  ここまでに基本的なごみ集め手法の原理につ

いて述べたが,それぞれが長所と短所をもっていて,決定的にどの方法が優れている

ということは言えない.また,それぞれの方法につて弱点を軽減するような様々な改

良が提案され使われている.さらに,プログラムを実行する CPUとは別の CPUでご

み集めを行うアルゴリズムも多く研究されており,マルチプロセッサが一般的なもの

になってきた今後有望な方向であるといえる.さらに,分散機能や恒久記憶などを採

り入れたプログラミング言語においては,ポインタそのものが「他のノードの実体を

指している」「恒久記憶に格納された実体を指している」など複数種類に分かれるよう

になる.そのような言語の処理系では,それぞれの局所領域やシステム全体などに各

レベルごとに別個の手法を組み合せて全体の記憶管理を構成することも普通に行われ

る.これらの話題全般については参考文献を参照されたい.

Page 177: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

8.6. 練 習 問 題 177

8.6 練 習 問 題

8-1. 自分の手元で利用可能な言語処理系を 1つ取り上げ,その処理系での実行時の領

域配置図をわかる範囲で調べて作成せよ.例えば C言語であれば,関数へのポ

インタやその他の変数へのポインタを整数に強制変換させて表示させてみれば番

地がわかるのが普通である.ついでに,これらの番地に値を格納させようとして

みればどの領域が保護されているかもわかる.

8-2. 自分の手元で利用可能な言語処理系を1つ取り上げ,スタック上のスタックフレー

ムの配置図を調べて作成せよ.特に引数の積み順や局所変数の割り当て方などを

調べよ.これは問題 8-1のようにしてスタック上の変数の番地を調べて,その近

辺をダンプするようなプログラムを動かすとよくわかる.

8-3. 自分の手元で利用可能な言語処理系を 1つ取り上げ,その引数および返値の受け

渡し機構の実現,レジスタの活用方法などを調べよ.それには翻訳後のアセンブ

リコードを読むのがよい.値呼びだと簡単すぎるので,参照呼びや名前呼びなど

の言語を対象にした方が面白い.

8-4. 自分の手元で利用可能な,ブロック型スコープ規則をもつ言語の処理系を 1つ取

り上げ,その環境切換え機構が静的チェイン方式かディスプレイ方式かを調べて

みよ.それには,nレベル外側の手続きを呼び出すときにかかる時間と nレベル

外側で定義された変数をアクセスするのにかかる時間とをnを様々に変えながら

計測してみるとわかるはずである (なぜか).

8-5. 自分の手元で利用可能な,ヒープからの領域割り当てが行える言語の処理系を 1

つ取り上げ,どのような領域割り当て方式を行っているかを調べてみよ.領域を

割り当てたり返却したりしながら割り当てられてくる領域の番地を観察すればか

なりの程度推定できるはずである.

8-6. 自分の手元で利用可能な,ごみ集め機構をもつ言語の処理系を 1つ取り上げ,ど

のようなごみ集め方式を採用しているか推定してみよ.続いて,その方式の「弱

点」を責めてみて,確かに推定が合っていると思われるかどうか確認せよ.

Page 178: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス
Page 179: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

179

第9章 中間コード生成

ソースコードからの情報も揃い,目的コードの動作環境も設計できたので,いよいよ

コードを生成することができる.しかし,1章でも述べたように,生成されるコードが

どんなものであるべきかはコンパイラの用途その他の諸条件によって変ってくる.本

章ではこれらの話題についてまとめた後,各種の言語機構に対応する具体的なコード

生成について解説する.

9.1 目的コードと中間コード

コンパイラの目的コードのあり方は,その用途によって様々である.目的コードは対

象とする計算機の機械語に対応したコード (アセンブリコード,再配置可能オブジェク

ト,実行形式のいずれか)である場合が多いが,そうでない場合もある.機械語に対応

しない目的コードは直接対象計算機で実行できない代り,次の利点をもつ.

• 目的コードをコンパクトなものとしやすい.

• 目的コードを特定の計算機に依存しないものにできる.

• 目的コードの生成をやさしくしやすい.

いずれにせよ,このような目的コードはコンパイラによって出力された後,次のいず

れかの方法で実行される.

• インタプリタに読み込み,解釈実行する.

• さらに実行可能な機械語に変換してから実行する.

後者の場合にはそのようなコードを「目的」コードとよぶのに抵抗があるかもしれな

いが,同一形式のコードをある場合にはインタプリタによって実行し,別の場合には

Page 180: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

180 第 9章 中間コード生成

機械語に変換する,ということも実際に行われる.一方,コンパイラ内部で目的コー

ドとは別種の中間コードを生成し処理するコンパイラも多い.中間コードを経る理由

としては次のものがあげられる.

• 目的コードになってしまうと最適化が行いにくい場合が多い.

• 機械依存の目的コードを扱う部分をできるだけ減らしたい.

実際には「中間コード」と「機械語に対応しない目的コード」の形式に本質的な違い

があるわけではなく,あるコード形式を中間コードとよぶか目的コードとよぶかはコ

ンパイラの内部での位置付けによって決まる面が大きい.以下本章では便宜上機械語

に対応しないようなコードを中間コードとよび,その各種形態と生成方法について解

説する.

9.2 様々な中間コード

9.2.1 木構造の中間コード

コンパイラ内部で抽象構文木を組み立てた後,それをそのままファイルに出力するか,

あるいはそれをもとに最適化を行った後目的コードを生成するコンパイラも多く見ら

れる.この場合は抽象構文木そのものが中間コードであると見なせる.木構造の中間

コードを採用する利点として次のものが考えられる.

• 抽象構文木を組み立てることは意味解析の一部としてごく機械的に行えるので,

中間コードの生成が容易である.

• 木構造には豊富な情報を付随させることができ,その情報を後段にそのまま渡す

ことができる.

一方で,木構造は比較的ソースコードに近い高レベルの表現であるため,命令の実行

順序などに関連した低レベルの最適化は行いにくい.このため,木構造を中間コード

として採用する処理形は次のいずれかの形態が多い.

• こみ入った最適化は行わず,中間コードをもとにただちに目的コードを生成する

ことで翻訳速度を高める.

Page 181: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.2. 様々な中間コード 181

a 1

+

a 1

+

*

:=

x

a 1

+

*

:=

x

図 9.1: 木と非循環有向グラフ

• 木構造を読み込んで解釈実行するようなインタプリタと組み合せて,なるべく簡

潔かつ機械独立な言語処理形をめざす.

• 木構造の中間コードを各種ツールないしプログラミング環境の共通プラットフォー

ムとして利用する.

最後の例としてはAda言語における標準的中間言語DIANAなどが典型的である.こ

の場合にはさらに 4つ組などより低レベルの中間コードへの変換と最適化を経て目的

コードに至ることが普通である.

9.2.2 非循環有向グラフ

木構造の弱点の 1つに,共通部分式 (common sub-expression,CSE)がうまく扱えな

い,というものがある.例えば

x := (a + 1) * (a + 1)

のようなコードに対する木を図 9.1左に示す.ここで木から素直にコード生成を行うと

a + 1の計算は同じ結果になるにも関わらず 2回実行される.後段の最適化でこれに

対処することもできるが,木構造に代えて非循環有向グラフ (directed acyclic graph,

DAG)を用いるこも有効なやり方である.非循環有向グラフは基本的には木であるが,

ただし複数の節が 1つの節を共通の子供としてもつことを許す.先の例を非循環有向

グラフで表したものを図 9.1右に示す.

上向き構文解析により非循環有向グラフを生成するのは原理的には簡単である.す

なわち,木を葉から根に向かって上向きに組み立てていく際,その各節の生成に先立っ

て,すでに同じ子供をもった同じ種類の節が存在するかどうか調べ,もしあれば新し

Page 182: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

182 第 9章 中間コード生成

い節をつくる代りにその節を用いる (つまり共有する)ようにすればよい.ただし実際

には,次の点にも注意を払わなければならない.

• 関数呼出しなどは関数が副作用をもつ可能性があるなら共有してはいけない.

• 字面上は同じ部分木でも,途中で値が変化する場合には共有してはいけない.

前者の例としては,例えば「入力を 1文字読みその文字を返す」という関数を考えれ

ばよい.後者は途中に変数の値を変更し得る手続き呼出しがはさまっていたり,変数

が別の変数と記憶領域を共有する (別名をもつ)場合まで考慮する必要がある.

9.2.3 後 置 コ ー ド

後置コード (postfix code)とは,演算命令においてオペランドがスタックから取られ,

演算結果もスタックに積まれるコード体系をいう 1.その特徴は次の通り.

• 演算命令に番地部が不要なのでコードがコンパクトである.

• 木からのコード生成が (特にテンポラリが不要なため)容易である.

例えば先に出てきた x := (a + 1) * (a + 1)の場合,対応する後置コードは次のよ

うなものになろう.

lda x -- xの番地をスタックに積むld a -- aの値をスタックに積むldc 1 -- 定数 1をスタックに積むadd -- a + 1 の計算ld a -- 再び aの値を積むldc 1 -- 再び 1を積むadd -- a + 1 の計算mul -- (a + 1) * (a + 1) の計算st -- 値を xに格納

この命令列が実行される様子を図 9.2に示す.

後置コードの命令体系をもつ仮想的な機械語を設計し,コンパイラが仮想機械語を生

成した後インタプリタ (仮想機械)によって実行するという方式 (仮想スタックマシン)

1オペランドをスタックに積む命令が先行し,その後に演算命令を置くことから後置コードとよばれる.

Page 183: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.2. 様々な中間コード 183

xの番地

aの値

xの番地

aの値

xの番地

a+1

xの番地

xの番地

a+1

aの値

xの番地

a+1

a+1

xの番地

結果

xの番地

a+1

aの値

図 9.2: スタックマシンによる実行の様子

の処理系が多く見られる.代表的なものとして Pascal-PのP-code,BCPLのOCODE,

LirithのM2マシンなどがある.仮想スタックマシンの利点としては,コードがコン

パクトなこと,レジスタや条件コードのようなものをもたないためインタプリタが保

持しなければならない状態情報が少なく,命令の形式が単純でインタプリタが高速化

しやすいことがあげられる.

さらに高速化のための工夫として,命令コードとして適当な数値を割り振る代りに

各命令についてその実行を担当する手続きを用意し,その番地を命令コードとするや

り方 (くし刺しコード — threaded code)がある.この場合,最初の命令が指している

番地に間接手続き呼出しを行うとその命令が実行を担当する手続きが呼ばれるが,そ

の戻り番地は次の命令を指していることになるので戻り命令に代って次の命令の呼出

しを行うことでオーバヘッドの少ない解釈実行が行える 2.

インタプリタ用コードとしてではなく,中間コードとして後置コードを利用する場

合もある.後置コードではスタックの状態と実行命令の位置が密接に関係しているの

で,コードの並換えを行うような最適化には向いていないが,コンパクトで移植可能

なコンパイラの開発をめざす場合には生成や変換の容易な後置コードの利点を活かす

ことができる.特に後置コードをそれと同等な動作を行う機械語の列に直接置き換え

ていくことで比較的簡単に機械語を生成するコンパイラが作成できる.Pascal-Pに基

づくこの方式のコンパイラが多くの種類の機械に移植されていて,代表的である 3.

2命令にオペランドが付随している場合にはそれは命令コードに続いて配置し,手続き中で戻り番地を用いてそれを参照した後その分戻り番地を進める.その効率は必ずしもよくないが,仮想スタックマシンではオペランドをもつ命令の比率は多くないはずである.

3単純な置き換えでは常にスタックからオペランドを取り出し,演算結果を再びスタックに格納するため

Page 184: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

184 第 9章 中間コード生成

9.2.4 4つ組と 3つ組

後置コードとは対象的に,全ての演算においてオペランドや結果を明示的に指定する

中間コード形式を 4つ組 (quadleple)とよぶ.先の x := (a + 1) * (a + 1)から生

成される 4つ組コードは例えば次のようになる.

(add a 1 t1) ; t1 ← a + 1

(add a 1 t2) ; t2 ← a + 1

(mul t1 t2 t3) ; t3 ← t1 * t2

(mov t3 x) ; x ← t3

ここで t1,t2などはテンポラリ (temporaly)とよばれ,途中結果を表すためにコン

パイラによって導入される変数である.これが「4つ組」とよばれるのは,各命令が

「操作,オペランド 1,オペランド 2,結果」の最大 4要素をもつためである.また命

令に加えて最大 3つのオペランド (番地)をもつことから 3番地コード (three address

code)とよばれることもある.4つ組では各演算の結果が明示的に名前をもつため,値

の定義と参照が前後しない限りにおいて自由に順序を入れ替えられ,共通部分式の値を

複数回計算する代りに一度計算した値を繰り返し参照するなどの記述も自然に行える.

4つ組において各演算の結果を明示的にテンポラリに代入しているのをやめ,演算結

果を参照したいときにはその演算を行う命令の番号によることもできる.この場合,各

命令は「操作,オペランド 1,オペランド 2」の 3要素になるのでこれを3つ組 (triple)

とよぶ.先の例を 3つ組で表すと次のようになろう.

(1) add a,1

(2) add a,2

(3) mul (1),(2)

(4) st (3),x

3つ組は 4つ組に比べてコンパクトであるが,命令の順序を入れ替えると命令番号

が代るため命令参照を修正しなければならないという弱点をもつ.このため最適化を

行う場合には通常 4つ組を用いる.

コードが長く,実行効率も悪くになりがちである.それに対処する方法として,11 章で述べる解釈実行型コード生成を用いることができる.

Page 185: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 185

9.3 中間コード生成の実例

9.3.1 枠 組 の 説 明

以下ではプログラミング言語の各種機能に対応した中間コード生成の概要を説明する.

コード生成の入力は抽象構文木,中間コードは 4つ組とするが,記述に Lispを使用す

るため全て S式で表現する.例えば x := a + 1であれば入力は

’(assign (var x) (add (var a) (const 1)))

のようになる.木には直接の構文情報に加えてコード生成の段階で必要となる各種情

報 (整数加算と実数加算の区別,データ型の大きさ,配列の上下限など)も付随してい

るものとする (修飾された構文木).コード生成を行う関数 genは

(gen 抽象構文木 種別)

の形で呼び出す.ここで「種別」は,どのようなコードを生成して欲しいかの情報を

渡すのに使用する.実際には genは木の節の種類ごとに,その種類を扱う関数を呼ぶ

だけである.これらの関数はその節の種類 (assign,var等)と同名にしておく.した

がって,genは次のように入力リストの先頭の記号から関数を取り出し,残りの部分

と種別を引数としてその関数を呼び出す.

(defun gen (s kind)

(apply (symbol-function (car s)) (cons kind (cdr s))))

生成コードは次に示す関数 emitにより広域変数*code*につけ加えていく.

(defun emit (s) (push s *code*))

関数 cgはまず*code*を空にしてから genを呼び,最後にできたコードを表示する.

(defun cg (s &optional kind)

(setq *code* nil) (setq *tempcount* 0)

(gen s nil) (for s (reverse *code*) (print s)))

なお,*tempcount*はテンポラリ番号のカウンタである.テンポラリには名前 T1,

T2…を用いることとし,これを割り当てる関数 newtempを用意する.

(defun newtemp () (newsym "T" (incf *tempcount*)))

Page 186: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

186 第 9章 中間コード生成

戻り番地

静的チェイン

動的チェイン

x

q

a

j

k

p1

p2

p3

t1

t2

fp

( )

実引数渡し用領域

テンポラリ領域

局所変数領域

仮引数領域

図 9.3: コード生成の実例に使用するスタックフレーム

Page 187: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 187

式のコード生成では「テンポラリを生成してそこに値を計算する」という手順が頻

繁に現れるため,これをまとめて行う関数 valtempを用意する.

(defun valtemp (s &aux x)

(setq x (newtemp)) (emit (append s (list x))) x)

例えば (valtemp ’(add i 1))とするとテンポラリ (例えば T1)がつくられ,コード

(ADD I 1 T1)が emitされ,値として T1が返される.

記憶域の構成は図 9.3のようなフレーム構造を前提とし,局所変数とテンポラリは

スタック上に置き,引数もスタック上で受け渡す.スタックの最大使用量はあらかじ

め計算できるので,それに基づいて引数領域も局所変数と同様にあらかじめ割り付け,

P1,P2,…の名前で参照する.動的チェインや静的チェインも本章の例では用いてい

ないが,必要なら同様に名前をつけてアクセスできる.これらの領域は目的コード上

では全てフレームポインタを起点としてアクセスするが,中間コード上では単純に名

前で参照できるものとしておく.変数や引数は本来コンパイラの方で一意に名前を割

り振るが,ここでは見やすさのためソースコード上の名前をそのまま使い,その代り

テンポラリ等と同じ名前を使わないよう注意する.

9.3.2 データ構造の参照と代入

a. 代入と値の複写  コード生成の立場から見たとき,値には基本型の値と構造

型の値の 2種類がある.前者は数値型,文字型,論理型などの値であり,計算機のレジ

スタに保持でき,中間コード命令の機能として代入や複写が行える.ポインタ (番地)

値はソース言語上では構造型とするのが普通だが,コード生成の立場からは基本型で

ある.基本型の代入操作に対応する関数 assignを次に示す.

(defun assign (kind l r &aux l1 r1)

(cond ((eq (car l) ’var)

(setq r1 (gen r ’value)) (emit ‘(mov ,r1 ,(cadr l))))

(t

(setq r1 (gen r ’value)) (setq l1 (gen l ’addr))

(emit ‘(mov* ,r1 ,l1)))))

基本型の代入の場合,左辺が単純変数であれば右辺の値を求めるコードを出し (その

結果としてその値に対応するテンポラリまたは変数の名前が返される),続いて mov命

Page 188: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

188 第 9章 中間コード生成

令を出す.左辺が単純変数でなければ左辺のアドレスを求め,mov*により間接代入を

行うようにする.

一方,構造型は配列やレコードなどに相当する.これらの値は一般にレジスタには

入り切らない.これらの値を転送するには,転送元と転送先の先頭番地とデータの大

きさ (バイト数)を引数としてブロック転送補助手続きを呼ぶことにする 4.構造型の

代入は節 cassignによって表すこととし,これに対応する関数を次に示す.

(defun cassign (kind l r size &aux l1 r1)

(setq r1 (gen r ’addr)) (setq l1 (gen l ’addr))

(emit ‘(mov ,r1 p1)) (emit ‘(mov ,l1 p2))

(emit ‘(mov ,size p3)) (emit ‘(libcall bcopy)))

すなわち左辺,右辺とも番地を求め,それを第 1 引数および第 2 引数に入れる.

cassignの節にはデータの大きさが付加されているものとして,それを第 3 引数に

入れる.その後 libcallでブロック転送用ルーチンを呼ぶ.以下に実行例を示す.実

際に実行するには varに対応する関数が必要だが,それは次項で説明する.

>(cg ’(assign (var x) (var y)))

(MOV Y X)

>(cg ’(cassign (var x) (var y) 100))

(ADDR Y T1)

(ADDR X T2)

(MOV T1 P1)

(MOV T2 P2)

(MOV 100 P3)

(LIBCALL BCOPY)

実際には基本型でも整数と実数の各種ビット長についてさらに区別する必要がある

が,本章では略した 5.

b. 定 数  整数や実数など基本型の定数は中間コードにおいても定数として書

けるものとする.それを扱う関数 constを示す.

4中間コードにブロック転送命令を含めることも可能だが,中間コード命令の種類を少なく保つためそうする.ブロック転送命令が備わった機械の場合には,目的コード生成時に補助手続き呼出しを機械語命令に置き換えることもできる.

5言語によっては符号つき整数と符号なし整数を区別する.またポインタ値も区別すべきである.

Page 189: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 189

(defun const (kind v &aux t1)

(cond ((eq kind ’addr)

(setq t1 (valtemp ‘(mov ,v))) (valtemp ‘(addr ,t1)))

(t v)))

種別が番地 (引数機構が参照渡しのみの言語で必要となる)の場合は,その値をいっ

たんテンポラリに入れ,その番地を別のテンポラリに入れて返す.そうでない場合は

直接定数値そのものを返せばよい.実行例を示す.

>(cg ’(assign (var x) (const 10)))

(MOV 10 X)

>(cg ’(cassign (var x) (const 10) 4))

(MOV 10 T1)

(ADDR T4 T2)

(MOV T2 P1)

(ADDR X T3)

(MOV T3 P2)

(MOV 4 P3)

(LIBCALL BCOPY)

レコードなど構造型の定数ではその値を主記憶上に用意する.したがって構造型の

定数はコード生成の段階では値が初期設定ずみの構造型の変数と同等である.ただし

文字列は構造型の一種であるが,ここでは読みやすさのため,中間コード上に直接文

字列を書くことを許し,それはその文字列を格納した番地を意味するものとする (実際

そのような機能を備えたアセンブラもある).そのため文字列定数の節は次のように別

扱いにする.

(defun sconst (kind v) (valtemp ‘(addr ,v ,t1)))

>(cg ’(sconst "abc"))

(ADDR "abc" T1)

なお,構造型全般について,許される種別は番地のみであるが,そのような整合性

の保証は意味解析が行うものとして特に検査はしない.

c. 変 数  局所変数の扱いはテンポラリと同等で,値が欲しいときは直接その

名前で中間コードから参照する.番地が欲しいときはテンポラリを用意しそこに addr

命令で番地を入れる.

Page 190: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

190 第 9章 中間コード生成

(defun var (kind v) (if (eq kind ’addr) (valtemp ‘(addr ,v)) v))

実行例はここまでに繰り返し出てきた.

広域変数や中間レベル変数の場合は記憶域の設計に依存するが,基本的にはまず番

地を求め (ブロック構造言語で中間レベルの変数ではここでディスプレイや静的チェイ

ンを参照する),続いて参照ではその番地から値を読み出し,代入ではその番地に値を

格納する.ここでは中間レベル変数は略し,広域変数のみを gvarという関数で扱う.

(defun gvar (kind v &aux t1)

(setq t1 (valtemp ‘(gaddr ,v)))

(if (eq kind ’addr) t1 (valtemp ‘(deref ,t1))))

gaddrという中間コード命令は広域変数の番地をテンポラリに転送する.目的コード

ではこれは単なる整定数のロードになるかもしれないし,広域変数用ベースレジスタ

とオフセットの加算になるかもしれない.derefはポインタを 1段たどる命令である.

実行例は略す.

d. 引 数  ここでは呼ばれ側で引数をアクセスする場合について説明する (呼

び側については呼出しの項で述べる).値呼びの場合には,そのスタック上の位置が違

うだけで,テンポラリや局所変数と同等に参照/代入してよい.複写復元呼びの場合は

戻った後呼び側で値をコピーし戻すところが違うだけだから,呼ばれ側では値呼びと

同じである.したがってこれらのコードは変数と同様 varによるものとする.

参照呼びでは引数の番地が渡される.したがってこの番地に対して間接参照/間接代

入を行えばよい.参照呼び引数は抽象構文木上では rvarという節で表すものとし,対

応する関数を示す.

(defun rvar (kind v) (if (eq kind ’value) (valtemp ‘(deref ,v)) v))

>(cg ’(assign (rvar x) (rvar y)))

(DEREF Y T1)

(MOV* T1 X)

名前呼びの場合にはサンクが手続き引数として渡されるので,まずそれを呼ぶ (呼出

しの項参照).その結果実引数の番地が返されるので,あとは参照呼びと同じとなる.

e. 配 列  以下は構造型なので,まずその先頭番地を求める.配列が局所変数,

広域変数,参照渡し引数のいずれの場合でも,genに種類として addrを渡せば番地が

Page 191: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 191

求められる.配列型の値を全体として扱う (複写等)場合にはこの番地を用いればよい.

一方要素を参照する場合には,要素の番地は次の式により定まる.

b+ (i− l) ∗ s

ただし bは配列の先頭アドレス,iは添字式の値,lは添字の下限値,sは 1要素の大

きさである.値が欲しい場合には番地が求まった後で間接参照する.

n次元配列の場合でもそれを「配列の配列」として扱う場合には上の式を各次元に

対して順次適用すればよいが,一括して計算する場合には次の式による.

b+ (i1 − l1) ∗ s1 + (i2 − l2) ∗ s2 + . . .+ (in − ln) ∗ sn

ixは x次元目の添字値,lxは対応する下限値を示す.ここで多くの言語では snが 1要素

の大きさ,sx−1 = sx×(ux− lx+1)(ただしuxは x次元目の添字の上限値)となる (行順

序— row wise).Fortranだけは逆で,s1が 1要素の大きさ,sx+1 = sx× (ux− lx+1)

となる (列順序 — column wise).

以下では簡単のため,多次元配列は配列の配列として扱う.また,添字の上限,下

限,1要素の大きさの情報は木に付随していて,(aind 配列 添字 下限 上限 大きさ)

のように表されているものとする (上限の情報はここでは使わないが,配列の範囲検査

コードを生成する場合には必要になる).

(defun aind (kind a i lb ub sz &aux a1 i1 t1 t2 t3)

(setq i1 (gen i ’value))

(setq t1 (valtemp ‘(sub ,i1 ,lb)))

(setq t2 (valtemp ‘(mul ,t1 ,sz)))

(setq a1 (gen a ’addr))

(setq t3 (valtemp ‘(add ,a1 ,t2)))

(if (eq kind ’value) (valtemp ‘(deref ,t3)) t3))

>(cg ’(assign (aind (var a) (var i) 1 100 4)

(aind (var a) (var j) 1 100 4)))

(SUB J 1 T1)

(MUL T1 4 T2)

(ADDR A T3)

(ADD T3 T2 T4)

(DEREF T4 T5)

(SUB I 1 T6)

Page 192: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

192 第 9章 中間コード生成

(MUL T6 4 T7)

(ADDR A T8)

(ADD T8 T7 T9)

(MOV* T5 T9)

f. レコード/ユニオン  レコード/ユニオンも構造型なので全体として扱うとき

は配列と同様である.レコードで各欄を参照する場合は,先頭番地に欄のオフセット

を加えてその欄の番地を求める.ユニオンは全ての欄のオフセットが 0のレコードと

考える.これらを節 offsetで表す.

(defun offset (kind r off &aux r1 t1)

(setq r1 (gen r ’addr))

(setq t1 (valtemp ‘(add ,r1 ,off)))

(if (eq kind ’value) (valtemp ‘(deref ,t1)) t1))

>(cg ’(assign (offset (var r) 4) (offset (var r1) 8)))

(ADDR R1 T1)

(ADD T1 8 T2)

(DEREF T2 T3)

(ADDR R T4)

(ADD T4 4 T5)

(MOV* T3 T5)

g. その他の型  他の型の場合も基本的な考え方は上と同じである.例えば集合

型などの構造型はその要素を直接アクセスする機能はなく,先頭アドレスを求めて全

体として扱う場合は配列やレコードと同じである.列挙型など基本型に準じるものは

その内部表現は整数と同等なので参照や代入も整数と同じでよい.文字列は言語によっ

て扱いが多少違うが,概ね配列と同様に扱えばよい.

9.3.3 式

a. 直接操作とライブラリの使い分け  式の本質的な部分は,定数や変数からの

値に演算を施す点にある.加算などの基本演算は直接対応する中間コード命令によっ

て実現する.一方,文字列の比較とか集合の要素判定など高レベルで複雑なものは

• 中間コードにそのような高レベルの演算命令を含める.

Page 193: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 193

• 中間コードの複数の命令を組み合せて高レベルの演算を実現する.

• ライブラリルーチンの呼出しに翻訳する.

から適切なものを選択する.ここでは簡単のためライブラリ呼出しを前提とする.

b. 算術式  算術式の演算子は直接中間コードの演算子に対応する.ここでは加

算に対応する addのみ示すが,他の演算も同様である.

(defun add (kind l r &aux l1 r1 t1)

(setq l1 (gen l ’value)) (setq r1 (gen r ’value))

(setq t1 (valtemp ‘(add ,l1 ,r1)))

(if (eq kind ’addr) (valtemp ‘(addr ,t1)) t1))

>(cg ’(assign (var x) (add (var y) (mul (var z) (const 5)))))

(MUL Z 5 T1)

(ADD Y T1 T2)

(MOV T2 X)

C言語ではポインタ値に対する加減算が可能だが,その場合ポインタが指す型の大

きさで右辺をスケーリングする.機械語命令に対応させられない演算 (例えばベキ乗な

ど)はライブラリ呼出しに翻訳する.また,プログラムの字面上には現れないが整数と

実数の間での自動変換も演算子の一種として扱う 6.

c. 比較演算と論理式  数値の比較演算に対しては指定条件が成立したときラベ

ルへ分岐する命令,すなわち

(ifgt オペランド 1 オペランド 2 ラベル)

などを生成する.多くの言語では if文などの条件部には論理式を記述するので,それ

に忠実に従うなら比較演算の結果を論理値として表す必要がある.以下では整数の 1

と 0で論理値の Trueと Falseをそれぞれ表すものとし,例えば演算子「>」に対応す

る関数は次のようになる (gt1という名前は,後述する飛越し型のものと区別するため

である).

(defun gt1 (kind l r &aux g1 g2 l1 r1 t1)

(setq g1 (newlabel)) (setq g2 (newlabel))

(setq l1 (gen l ’value)) (setq r1 (gen r ’value))

(setq t1 (newtemp))

6自動変換演算に対応する節は意味解析によって挿入されるものとする.

Page 194: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

194 第 9章 中間コード生成

(emit ‘(ifgt ,l1 ,r1 ,g1))

(emit ‘(mov 0 ,t1)) (emit ‘(jmp ,g2)) (emit g1)

(emit ‘(mov 1 ,t1)) (emit g2)

(if (eq kind ’addr) (valtemp ‘(addr ,t1)) t1))

すなわち,テンポラリを 1個用意し,そこに条件の成否に応じて 0または 1を入れ

る.他の比較演算についても同様である.実行例は次の通り.

>(cg ’(assign (var x) (gt1 (var y) (const 1))))

(IFGT Y 1 L1)

(MOV 0 T1)

(GOTO L2)

L1

(MOV 1 T1)

L2

(MOV T1 X)

このように表現しておけば,and,or,notの各論理演算は中間コードの対応する論理

演算に対応させられるので,その扱いは四則演算の場合と同じである.

d. 論理式の飛越し型表現  前節の方法では論理式に含まれる全ての条件式が評

価され,その真偽が対応する 0か 1に変換された後で論理演算が実行される.しかし

andで結合された条件式の左側が偽ならば,右側が何であっても andの結果は偽であ

り,右側の条件の評価は本来不要である.同様に orの場合も左側の条件を評価して真

であれば右側の条件は評価する必要がない.このように不要な評価を回避するコード

を論理式の飛越し型コード (short curcit code)とよぶ 7.

飛越し型コードを実装する際,真理値を 0と 1に直した後それに基づいて飛び越す

ようにもできるが,それでは二度手間であり,ifgtなどの行き先ラベルを直接利用し

て飛び越すようにしたい.そのためには genに対して「飛び越したときがTrueで下に

抜けてきたときが Falseなのかその反対か」という情報と飛び先ラベルを情報として

渡す必要がある.ここでは「種別」のところにこの 2つの情報を並べたリストを渡す.

これを用いた「>」に対応する関数は次のようになる.

(defun gt2 (kind l r &aux g1 l1 r1)

7Pascal では論理式に含まれる式は全て評価されることと定められているので,飛越し型コードは使えない.Fortran では飛越し型コードの採用は処理系作成者に任される.C や Ada では飛越し型コードに対応する論理演算子が別途用意されているので,必ず飛越し型コードを実装しなければならない.

Page 195: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 195

(setq l1 (gen l ’value)) (setq r1 (gen r ’value))

(if (car kind)

(emit ‘(ifgt ,l1 ,r1 ,(cadr kind)))

(emit ‘(ifle ,l1 ,r1 ,(cadr kind)))))

すなわち,条件が真のとき飛びたいか偽のとき飛びたいかで中間コード命令を選択

する.これだけでも前の版よりはずっと簡単だが,飛越し型コードの真価は andなど

による条件の結合が容易な点である (原理的に効率もよい).それらに対応する関数を

示す.

(defun and2 (kind l r &aux g1)

(cond ((car kind)

(setq g1 (newlabel)) (gen l (cons nil g1)) (gen r (cons nil g1))

(emit ‘(jmp ,(cadr kind))) (emit g1))

(t (gen l kind) (gen r kind))))

(defun or2 (kind l r &aux g1)

(cond ((car kind)

(setq g1 (newlabel)) (gen l (cons t g1)) (gen r (cons t g1))

(emit ‘(jmp ,(cadr kind))) (emit g1))

(t (gen l kind) (gen r kind))))

(defun not2 (kind l &aux g1)

(gen l (cons (not (car kind)) (cadr kind))))

例えば andの場合「真で飛び越すように」と言われた場合にはは両側の条件とも真

なら下へ抜けてくるようにしておいてそこで真のときの行き先に飛び,最後に偽のとき

下へ抜けるためのラベルを置く.「偽の場合に飛び越すように」であれば,それは両側の

条件に対応するコード列の条件と同じだから,単に両者を連続させるだけでよい.or

の場合はその反対になる.notは単に飛越しの真偽を逆にして genを呼ぶだけである.

(cg ’(and2 (gt2 (var x) (const 1))

(gt2 (var y) (const 2))) ’(t . l0))

(IFLE X 1 L1)

(IFLE Y 2 L1)

(JMP L0)

L1

上で説明したように,andで真で飛び越す方は条件を入れ替えるための jmpとラベ

ルが必要になる.運がよければ次のように複雑でも余計なラベルは不要である (これは

(x > 1) and not ((y > 2) or (z > 3))で偽のとき L0に飛ぶ例である).

Page 196: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

196 第 9章 中間コード生成

>(cg ’(and2 (gt2 (var x) (const 1))

(not2 (or2 (gt2 (var y) (const 2))

(gt2 (var z) (const 3))))) ’(nil L0))

(IFLE X 1 L0)

(IFGT Y 2 L0)

(IFGT Z 3 L0)

論理値について 2通りの表現がある場合,それらの間を行き来することが必要にな

る.これを行う関数を示す.

(defun conv-sc-01 (kind l &aux g1 g2 t1)

(setq g1 (newlabel)) (setq g2 (newlabel)) (setq t1 (newtemp))

(gen l (cons t g1)) (emit ‘(mov 0 ,t1)) (emit ‘(jmp ,g2))

(emit g1) (emit ‘(mov 1 ,t1)) (emit g2) t1)

すなわち,飛越し表現から 0/1にするにはテンポラリを用意し,下に抜けてきたと

きと飛び越したときで別の値をこれに入れればよい.この反対は,

(defun conv-01-sc (kind l &aux l1)

(setq l1 (gen l ’value))

(if (car kind)

(emit ‘(ifne ,l1 0 ,(cdr kind)))

(emit ‘(ifeq ,l1 0 ,(cdr kind)))))

値を 0かどうかテストして飛び越すが,真偽どちらで飛び越すかによって使う命令

が逆になる.「b1 := (x > 1) and b2」という論理代入文に相当する例を示しておく.

>(cg ’(assign (var b1)

(conv-sc-01 (and2 (gt2 (var x) (const 1))

(conv-01-sc (var b2))))))

(IFLE X 1 L3)

(IFEQ B2 0 L3)

(JMP L1)

L3

(MOV 0 T1)

(JMP L2)

L1

(MOV 1 T1)

L2

(MOV T1 B1)

Page 197: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 197

確かにこれで正しいが,ややコードが長い.これは条件が andで結合されているのに

conv-sc-01が「真で飛び越す」指定をするためである.これを避けるにはconv-sc-01

で最上位レベルの条件結合を調べて選択するか,または「真偽どちらでも都合のよい

飛越しコードをつくれ」という指定ができるようにする.

コード生成を見通しよくするうえでは,どこで 0/1表現と飛越し表現のどちらを使

うかを意味解析に選択させ,これらの間の変換も挿入してもらうのがよさそうである.

ちなみに,gt2と conv-sc-01とを組み合せれば gt1と同等のコードが得られること

も注意しておきたい.

9.3.4 制 御 構 造

a. 文の並び  文の並びについては,単に並んでいる各文のコードを順次生成す

ればよい.文の並びは文法では通常次のような形になる.

stlist = nil | stlist stat

抽象構文木もこれに対応したものとすれば,コード生成関数は次のようにすればよい.

(defun stlist (kind l s) (if l (gen l kind)) (gen s kind))

>(cg ’(stlist (stlist nil (assign (var x) (const 1)))

(assign (var y) (const 2))))

(MOV 1 X)

(MOV 2 Y)

b.  if文   if文のコードは条件に従ってコードの一部をまたぎ越すだけであり,

論理式の飛越し型コードが実装されていれば次のように簡単である (されていない場合

は conv-01-scと同様のことを自前でやればよい).なお,else部のある場合とない場

合で節は区別されるものとした.

(defun ifstat1 (kind c s1 &aux g1)

(setq g1 (newlabel)) (gen c (cons t g1)) (gen s1 kind) (emit g1))

(defun ifstat2 (kind c s1 s2 &aux g1 g2)

(setq g1 (newlabel)) (gen c (cons t g1)) (gen s1 kind) (emit g1)

(setq g2 (newlabel)) (emit ‘(jmp ,g2)) (gen s2 kind) (emit g2))

Page 198: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

198 第 9章 中間コード生成

>(cg ’(ifstat1 (gt2 (var x) (var y)) (assign (var x) (var y))))

(IFGT X Y L1)

(MOV Y X)

L1

>(cg ’(ifstat2 (gt2 (var x) (var y))

(assign (var x) (var y)) (assign (var y) (var x))))

(IFGT X Y L2)

(MOV Y X)

L2

(JMP L3)

(MOV X Y)

L3

if 文の終りが endifなどで明示されている形の構文をもつ言語では 1 つの if文

に else-ifの連鎖が含まれるが,抽象構文木の上では旧来のものと同じ形にしてしま

える.

c. ループ構文と脱出   loop文,while文,repeat-until文などは全て条件

部の有無や位置が違うだけであとは酷似している.代表して while文の例を示す.

(defun whilestat (kind c s1 &aux g1 g2)

(setq g1 (newlabel)) (setq g2 (newlabel))

(emit g1) (gen c (cons nil g2)) (gen s1 (cons g1 g2))

(emit ‘(jmp ,g1)) (emit g2))

>(cg ’(whilestat (lt2 (var x) (const 10))

(assign (var x) (add (var x) (const 1)))))

L1

(IFGE X 10 L2)

(ADD X 1 T1)

(MOV T1 X)

(JMP L1)

L2

for文は,Cの場合は whileの類似品であるが,Pascalや Adaの for文ではループ

変数の初期設定や更新のコードも出す必要がある.ここでは簡単のため,その場で代

入や条件式に相当する構文木を組み立てこれまでにつくったコード生成部を利用する.

(defun forstat (kind v e1 e2 e3 s1 &aux g1 g2 g3)

(setq g1 (newlabel)) (setq g2 (newlabel)) (setq g3 (newlabel))

Page 199: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 199

(gen ‘(assign ,v ,e1) nil)

(emit g1) (gen ‘(le2 ,v ,e2) (list nil g2))

(gen s1 (list g3 g2)) (emit g3)

(gen ‘(assign ,v (add ,v ,e3)) nil) (emit ‘(jmp ,g1)) (emit g2))

>(cg ’(forstat (var x) (const 1) (const 10) (const 1)

(assign (aind (var a) (var x) 1 10 4) (const 0))))

(MOV 1 X)

L4

(IFGT X 10 L5)

(SUB X 1 T1)

(MUL T1 4 T2)

(ADDR A T3)

(ADD T3 T2 T4)

(MOV* 0 T4)

L6

(ADD 1 10 T5)

(MOV T5 X)

(JMP L4)

L5

ループ構文でもう 1つの問題は,ループから脱出する文と,ただちに次の周回に進

む文 (Cでいえば breakと continue)の扱いである.これらの文は常に最内側のルー

プ (Cの breakは switch文の抜出しにも使うので switch文も含める)の抜出しにな

るので,ループ本体のコード生成中に常に「現在 break/continueが出てきたら飛ぶ

べきラベルはどこか」という情報を保持していなければならない.

ここでは安直な実現方法として,「ループの中の文については,種類の代りに 2つの

ラベルのリストを渡す」という方法を示す.実はここまでに示した whilestatなどの

関数はすでにそのようになっているし,複合文に対応する関数は全て渡された種類を

そのまま中の文にも渡すようになっている.その場合 break文/continue文のコード

生成は次のようにできる.

(defun breakstat (kind)

(emit ‘(jmp ,(cadr kind))))

(defun contstat (kind)

(emit ‘(jmp ,(car kind))))

>(cg ’(whilestat (gt2 (var x) (const 0))

Page 200: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

200 第 9章 中間コード生成

(stlist (stlist nil (assign (var x) (add (var x) (const 1))))

(ifstat1 (eq2 (var x) (var y)) (breakstat)))))

L3

(IFLE X 0 L4)

(ADD X 1 T1)

(MOV T1 X)

(IFNE X Y L5)

(JMP L4)

L5

(JMP L3)

L4

これはあくまで原理を示すための一例であり,実際には意味解析の段階で break文

や continue文が確かにループ中で使われているかどうか検査するべきである.その段

階でラベルも割り当ててその情報を木に含めておけば単にそのラベルへ分岐するコー

ドを生成するだけですむ.言語によっては「nレベル外側のループから抜ける」「…と

いう名前で識別されるループから抜ける」などの機能を提供するものもあり,その場

合には各実行文においてそれを含む全ループの情報が必要なため,特にそう言える.

d.  case構文   case構文は 1つの式の値に応じて複数のラベルのどれかに分

岐するもので,通常は式の型は整数や文字などに制限されている.その実現方法とし

て最も簡単なのは if-then-elseと同様行き先ごとに逐一式の値と比較していくこと

である.とりあえずこの方法による例を示す.ここでは caseのラベルは整定数に限ら

れ,また case文の本体は

((値のリスト 文) (値のリスト 文) ...)

という形をしているものとする.

(defun casestat (kind e1 body &aux l1 g0 g1 b1)

(setq g0 (newlabel))

(setq l1 (gen e1 ’value))

(for arm body

(setq g1 (newlabel))

(for e0 (car arm) (emit ‘(ifeq ,l1 ,e0 ,g1)))

(push (cons g1 (cdr arm)) b1))

(emit ’(move "case-range" p1)) (emit ’(libcall raise))

(for arm (reverse b1)

(emit (car arm)) (gen (cdr arm) kind) (emit ‘(jmp ,g0)))

(emit g0))

Page 201: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 201

すなわち,まず case文の末尾の飛び先ラベルを用意し,式の値を評価したあと,case

の各枝ごとにラベルを生成し,その枝についている定数全てについて

(ifeq 値 定数 ラベル)

を出力するとともに,このラベルとその枝に対応する文を合せて b1というリストに覚

えておく.全部の枝の処理が終って下へ抜けてきた場合はどのラベルでもなかったこ

とになるので,"case-range" という例外を出す (例外については後述).どのラベル

でもなかったときの処理が指定できる言語では例外を出す代りにその部分のコードを

ここに入れる.以上で分岐部分は終ったので,今度は b1に覚えた情報をもとに各枝ご

とにラベル,文,末尾への分岐をこの順に出す.

case x of

1, 2, 3: y := 0;

4, 5, 6: y := 1

end

に対応するコード生成の実行例は次の通り.

>(cg ’(casestat (var x)

(((1 2 3) (assign (var y) (const 0)))

((4 5 6) (assign (var y) (const 1))))))

(IFEQ X 1 L2)

(IFEQ X 2 L2)

(IFEQ X 3 L2)

(IFEQ X 4 L3)

(IFEQ X 5 L3)

(IFEQ X 6 L3)

(ADDR "case-range" P1)

(LIBCALL RAISE)

L2

(MOV 0 Y)

(JMP L1)

L3

(MOV 1 Y)

(JMP L1)

L1

この方法は明らかにコードの実行効率は良くない.他の方法としてラベルの配列を

用意し,式の値を添字としてこの配列を参照して分岐先を求めるようにもできる.こ

Page 202: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

202 第 9章 中間コード生成

の場合,値の範囲がそう大きくない場合 (例えば列挙型,論理型,文字型など)にはよ

いが,範囲が広すぎると配列が巨大になる.言語仕様の制約上そのような問題が起こ

らないのでない限り,比較型の実現にも切り換えられるようにするのがよい.大きさ

の問題がなくても,分岐の枝が少数だったり特定の枝が圧倒的に多く選ばれる場合に

は比較型の方が効率がよい可能性もある.凝ったやり方としては,頻繁に実行される

ような場合を数例比較型で判定し,残りはラベルの配列を参照するが,配列が大き過

ぎるようならある程度値が密な部分だけ配列にして,残った部分は再び比較型による

こともできる.

e. ラベルと goto文   goto文のコードは一見簡単そうに思えるが,場合によっ

ては単純ではない.局所的な gotoは例えば Pascalなどのブロック構造言語の場合,

gotoによって n段の手続きから抜け出すことができる.次の場合を考えてみる.

label 100;

procedure p1(...);

begin

...

goto 100; ←※end;

procedure p2(...);

begin

...

p1(...);

end;

begin

...

p2(...);

100: ... ←☆end;

ここで※印の gotoは手続き p2,p1の呼出しから戻って☆へ実行を移す必要がある.

そのためには問題のラベルが属しているレベルに到達するまで,スタックを 1フレー

ムずつ「巻き戻し」ていくことになる.さらに,スタックを戻しながら各呼出しの戻り

処理も行う必要がある 8.このように広域 goto(non-local goto)の処理は複雑であり,

8例えばディスプレイを使用しているなら 1段ずつディスプレイを復元して行かないと☆に戻ったときのディスプレイが正しくならないし,複写復元呼びを用いているなら 1フレームずつ引数を複写し戻さないと☆に戻った時実引数が更新されていないことになる.

Page 203: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 203

ライブラリルーチン呼出しにより実現するのがよさそうである.一方,広域的でない

(1つの手続き内での) goto の方は単に中間コードの jump命令に対応させればよい.

以下に上記の方針による実現例を示す.なお gotoのラベルはあらかじめコード生成

時につくられるラベルとは名前が重ならないように割り当てられ、構文木に付随して

いるものとする.

(defun ggoto (kind label level)

(emit ‘(addr ,label p1)) (emit ‘(mov ,level p2))

(emit ’(libcall nonlocalgo)))

(defun lgoto (kind label) (emit ‘(jmp ,label)))

(defun llabel (kind label) (emit label))

次のようなプログラム断片 (特に意味はないが)

LL0: if x = 0 then goto GL1;

x := x - 1;

goto LL0;

に対応するコード生成の例を示す (GL1への飛越しは広域 gotoであるものとする).

>(cg ’((llabel ll0)

(ifstat1 (eq2 (var x) (const 0)) (ggoto gl1 3))

(assign (var x) (sub (var x) (const 1)))

(lgoto ll0)))

LL0

(IFNE X 0 L2)

(ADDR GL1 P1)

(MOV 3 P2)

(LIBCALL NONLOCALGO)

L2

(SUB X 1 T2)

(MOV T2 X)

(JMP LL0)

f. 例外処理構文  新しい言語には例外処理構文をもつものが多い.その機能は

言語によって様々であるが,基本的には次の 2つの機能の組合せから成っている.

(a) 1つの手続きの中で,ある場所 (例外の起きた場所)から別の場所 (例外ハンドラ)

へ制御を移行する.

Page 204: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

204 第 9章 中間コード生成

(b) 1つの手続き (例外を起こした手続き)から,呼出し系列を遡って別の手続き (例

外を受けとめて処置できる手続き)へ制御を移行する.

実際には (b)の場合でも制御を移行する点は呼出した点とは違うので,(a)の機能も合

せもつことになる.一方 (a)の機能のみで (b)のない言語は十分可能である.そして,

結果的に起こることに着目すると (a)は前項の局所的 goto,(b)は広域的 gotoと類似

している.これらを考えると,例外が起きた場合には統一的にライブラリルーチンを

呼んで制御の移行を任せるのが自然である.ライブラリルーチンに渡すべき引数とし

ては次のものがあげられる (詳細は対象言語の仕様によって異なる).

• 例外の発生場所を表す情報 (ただしライブラリルーチンを「呼び出す」ので,そ

の戻り番地情報で用が足りるかもしれない).

• 例外の種類を表す情報 (整数の「例外コード」や文字列の「例外名」など).

• 例外ハンドラに引数を渡せるような言語ではその引数の情報.

• どの場所で発生したどの例外ならどこへ制御を移行するかを記した例外表.

ただし最後の例外表については,(a)の局所的な例外であれば直接渡すこともできる

が,(b)の場合には複数の手続きがからむので決まった広域変数から全手続きの例外表

を順次たどれるように設計する方がよさそうである.そのような例外表の構造の例を

図 9.4に示す.すなわち,手続きQの中で ex1という例外を発生させた場合,その処

理のため呼ばれるライブラリ手続き raiseはまず範囲表を参照して呼び元がQである

ことを見つける.さらに範囲表を探してもQの中には例外ハンドラはないので,この

例外処理のため呼出しを遡る必要がある.そこでスタックフレームを参照し,Qを呼

び出しているのはPであり,その呼出し位置はPの中のハンドラの処理範囲に含まれ

ていることがわかる.さらに例外表を参照すると,そのハンドラは例外名 ex1 を受け

止め,そのときの行き先はラベル L1であることまでわかる.行き先フレームとラベル

がわかれば,それが例外発生点と同じフレームなら局所的 gotoと同じに,そうでなけ

れば広域的 gotoと同様スタック巻戻しの後,制御を移せばよい.

このような例外表を採用し,例外に対する引数は渡せないものとした場合の例外発

生文に対するコード生成を次に示す.実行例は略す (生成されたコードは case文のと

ころで示した).

Page 205: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 205

proc P; A; begin B; call Q; except case-range: C; ex1: D; end; E;end P;proc Q; F; raise ex1; G;end Q;

P: AL0: B call Q goto L3L1: C goto L3L2: D goto L3L3: EQ: F mov "ex1" p1 libcall raise G

r1

r2

r3

r1 手続き

処理範囲

手続き

r2

r3

範囲 種別 付属例外数・ 例外表位置レベル

0

1

0

0

2

0

範囲表 例外表

名前ラベル

L1

L2

"case-range"

"ex1"

ソースコード オブジェクトコード

図 9.4: 例外表の例

(defun raisestat (kind name)

(emit ‘(addr ,name p1)) (emit ’(libcall raise)))

さらに,言語によっては手続き単位でその手続きが (例外によって)通常でない終り

方をした際の後始末コードが指定できるものもある.その場合は後始末コードの情報

は各スタックフレームの特定位置に置くようにするので,巻戻しの中途で後始末コー

ドの指定を発見したらそれを実行してやる.

ここまでは例外ハンドラの実行が終った後の実行はそのハンドラの先へ進む場合を

想定していたが,ハンドラが実行された後,例外を発生した場所に戻って実行を続け

る言語もある.この場合にはスタックを巻き戻してしまうわけにいかず,表から例外

ハンドラを見つけた後それを一般の手続きと同様に呼び出して実行させる.

Page 206: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

206 第 9章 中間コード生成

9.3.5 手  続  き

手続きの引数アクセスについてはすでに述べたので,本節ではそれ以外の話題につい

てまとめる.手続きの呼びと戻りそのものは,対応する中間コード命令を生成する.具

体的には呼ぶときは

(call オペランド)

なる中間コード命令を用いるものとする.呼ばれ側では手続きの入口 (prologue)で

スタックの調整やリンクの管理,出口 (epilogue)ではこれらの状態の復元と戻り命令

の実行が必要である.これらをそれぞれ

(proc 名前 スタック所用量)

(pend)

で表すものとする.return文のように本体のどこからでも戻れる言語の場合はそのつ

ど出口処理を重複するのではなく,出口の直前に決まったラベルを出しておいてここ

へ飛ぶようにする.ブロック構造の言語ではさらに環境チェインの管理が必要なので,

callと procのオペランドとして「レベル番号」を追加する必要がある.呼出し管理そ

のものはこれだけで,あとの主要な複雑さは引数渡しや戻り値に関連したものである.

a. 値渡しのコード  ライブラリルーチン呼出しと同様,基本型の値渡しはその

値を P1などスタック上の引数位置に入れてから手続きを呼び出せばよい.

(defun proccall (kind name &rest args &aux ano p v)

(setq ano -1)

(for a args

(setq p (nth (incf ano) ’(p0 p1 p2 p3 p4 p5 p6 p7 p8 p9)))

(setq v (gen a ’value)) (emit ‘(mov ,v ,p)))

(emit ‘(call ,name)))

>(cg ’(proccall sub1 (const 1) (var x) (add (const 1) (var y))))

(MOV 1 P0)

(MOV X P1)

(ADD 1 Y T2)

(MOV T2 P2)

(CALL SUB1)

Page 207: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 207

P1

P2

P3

P4

P0

D2

D1

D0

P5

図 9.5: 引数受け渡し位置の名前づけ

ところで,基本型でも整数が 4バイトで倍精度実数が 8バイトのように大きさが異

なる場合がある.このときは引数領域は全て 8バイト単位にする,4バイトと 8バイト

で詰め合せて,境界整合を行う/行わないなどの選択肢がある.これを中間コード上で

表現するには,例えば図 9.5のように引数領域に別名をつけておけばよい.また,引

数のレジスタ渡しを行う場合には,整数と実数で受渡しに用いるレジスタが異なる場

合が多い.そのような場合を考えると,大きさが同じでも P0と F0 のように別の名前

で参照しておく方が好都合である.

構造型の場合はその大きさから考えて,あらかじめスタックにその分の領域を用意

しておくことはあまり考えられない.引数ごとにスタックを伸ばす方式の延長として

スタックに直接積むようにもできるが,ここでは構造型については番地を渡し,呼ば

れた側で自分のスタックフレーム内に複写するものとする.もともと複写の手間を考

えれば番地で渡す手間はあまり問題にならない.

最後に手続き引数の問題がある.Cや Fortranなどでは単にその先頭番地を渡せば

すむが,ブロック構造をもつ言語では 8章で説明したように環境ポインタを合せて渡

す必要がある.その場合は 1つの手続き引数の受渡しに P0と P1というように 2つの

連続した場所を使用する.

b. その他の引数渡し  複写復元呼びの場合には呼ぶまでは値呼びと同じだが,

制御が戻ってきた後で呼ばれ側で書き換えたスタック上の引数をそれぞれ元の場所に

複写し戻す必要がある.そのためには Pnから元の場所への mov命令を生成する.参照

渡しの場合には引数が全部ポインタ値として渡されるため大きさが同じであり,値渡

しで出てきた問題がほとんど生じない.手続き引数のみは同様に扱う必要がある.名

前呼びは全引数が手続き引数 (つまりサンク)であるとして扱えばよい.ただしサンク

Page 208: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

208 第 9章 中間コード生成

の方もコード生成しておかないといけないが,その内容は参照渡しの場合と同様に番

地を計算し,その値を返すというものになる.

c. 返 値  返値は多くの言語では 1個だけなので,レジスタで受け渡すのが直

接的でわかりやすい.そこで中間コード上でも1つ名前を決め,これを返値用レジスタ

として使うものとする.ここでは rrという名前にしてある.この方式による return

文の実現 (値を返さないものと返すもの)を示す.

(defun returnst0 (kind) (emit ’(jmp lreturn)))

(defun returnst1 (kind e &aux v1)

(setq v (gen e ’value)) (emit ‘(mov ,v rr)) (emit ’(jmp lreturn)))

>(cg ’(returnst1 (var x)))

(MOV X RR)

(JMP LRETURN)

一方,返値を参照する関数呼出しは手続き呼出しとほぼ同様だが,ただし引数の式

が関数呼出しを含むときは途中まで用意した引数を壊さないように注意する必要があ

る (引数をそのつどスタックに積む方式であればすでに積んだ引数が壊される心配はな

い).ここではまず全引数を評価してテンポラリに入れるものとした (先の手続き呼出

しも同様にすべきである).あとは戻ってきた値を別のテンポラリに複写すればすむ (そ

うしておかないと,rrの値が次の関数呼び出しによって書き換えられる恐れがある).

(defun funccall (kind name &rest args &aux ano p t1 t2 a1)

(for a args (push (gen a ’value) a1))

(setq ano -1)

(for a (reverse a1)

(setq p (nth (incf ano) ’(p0 p1 p2 p3 p4 p5 p6 p7 p8 p9)))

(emit ‘(mov ,a ,p)))

(emit ‘(call ,name)) (setq t1 (newtemp)) (emit ‘(mov rr ,t1))

(cond ((eq kind ’addr)

(setq t2 (newtemp)) (emit ‘(addr ,t1 ,t2)) t2)

(t t1)))

>(cg ’(assign (var x) (funccall f1 (const 1) (funccall f2 (var y)))))

(MOV Y P0)

(CALL F2)

(MOV RR T2)

(MOV 1 P0)

(MOV T2 P1)

Page 209: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 209

procedure bubble(a:array[1..100] of integer; n:integer);

var notyet, i, x: integer;

begin

notyet := true;

while notyet do begin

notyet := false;

for i := 1 to n-1 do

if a[i] > a[i+1] then begin

x := a[i]; a[i] := a[i+1]; a[i+1] := x; notyet := true

end

end

end;

図 9.6: バブルソートのソースコード

(CALL F1)

(MOV RR T3)

(MOV T3 X)

以上は基本型のみを扱っていたが,基本型以外の返値はレジスタに入れられないの

で,次のどちらかの方法を取る.

• 呼ばれ側で返値を適当な場所に入れ,その番地をレジスタに入れて返す.

• 呼び側で返値を入れる領域を確保し,その番地を呼出し時に渡す.

前者では再度スタックを伸ばすと返値を壊すため,戻ったらすぐに戻り値を安全な場

所に複写する必要がある.なお,返値が 1個でなく複数個許される場合にも一連の戻

り値を 1つのレコードであると考えればこれらの方法が適用できる.

9.3.6 まとまった実行例と共通式の削除

これまでの例が細切れだったので,もう少し大きな例としてバブルソートを行う手続

きを取り上げる.ソースコードを図 9.6に示す.これに対応する抽象構文木から上で述

べてきたプログラム群により 4つ組コードを生成した結果を図 9.7に示す.これを見

ると,部分式の計算ごとにテンポラリが生成されるので非常にテンポラリの数が多く,

Page 210: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

210 第 9章 中間コード生成

(MOV 1 NOTYET) (DEREF T11 T12) (MOV* T23 T27)

L1 (IFLE T6 T12 L6) (ADD I 1 T28)

(IFEQ NOTYET 0 L2) (SUB I 1 T13) (SUB T28 1 T29)

(MOV 0 NOTYET) (MUL T13 4 T14) (MUL T29 4 T30)

(MOV 1 I) (ADDR A T15) (ADDR A T31)

L3 (ADD T15 T14 T16) (ADD T31 T30 T32)

(SUB N 1 T1) (DEREF T16 T17) (MOV* X T32)

(IFGT I T1 L4) (MOV T17 X) (MOV 1 NOTYET)

(SUB I 1 T2) (ADD I 1 T18) L6

(MUL T2 4 T3) (SUB T18 1 T19) L5

(ADDR A T4) (MUL T19 4 T20) (ADD I 1 T33)

(ADD T4 T3 T5) (ADDR A T21) (MOV T33 I)

(DEREF T5 T6) (ADD T21 T20 T22) (JMP L3)

(ADD I 1 T7) (DEREF T22 T23) L4

(SUB T7 1 T8) (SUB I 1 T24) (JMP L1)

(MUL T8 4 T9) (MUL T24 4 T25) L2

(ADDR A T10) (ADDR A T26)

(ADD T10 T9 T11) (ADD T26 T25 T27)

図 9.7: バブルソートの手続きの中間コード

また同じ計算を行うコードが多数生成されている.そこで 9.2節で説明した非循環有

向グラフの構成により,共通部分式の重複計算を抑制するよう改良する.まず,現在

どのテンポラリがどの値を保持しているかを覚えておくための変数*temp-cache*を導

入する.ラベルや手続き呼出しがあった場合には (他から飛び込んできた場合や手続き

内の副作用を考えると)それらをまたがって前に計算した値に依存することはできない

ので,*temp-cache*を空にするように emitを変更する.

(defun emit (s)

(if (or (atom s) (eq (car s) ’call)) (setq *temp-cache* nil))

(push s *code*))

そして,関数 valtempを修正し,テンポラリに値を計算するコードを出すと同時に

その情報を連想リストの形で*temp-cache*に追加する.ところで,ラベルなどでご破

産になる以外にも,前に計算した値を再利用しては困る場合がある.例えば i + 1の

値を計算してこれがテンポラリに入っているとしても,iへの代入があったらもはや前

の i + 1の値は正しくないので再利用することはできない.したがって,連想リスト

Page 211: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.3. 中間コード生成の実例 211

にはテンポラリとその値に加えて,その値がどの変数に依存しているかも覚えておく

必要がある.このため depcollectという補助関数を使用する.改良した valtempは

次の通り.

(defun valtemp (s &aux x a dep)

(for e *temp-cache* ; (1)

(if (equal (cddr e) s) (return-from valtemp (car e))))

(setq x (newtemp)) (emit (append s (list x))) ; (2)

(case (car s) ; (3)

((deref)

(setq dep (depcollect (cadr s))) (push ’* dep))

((add sub mul div)

(setq dep (union (depcollect (cadr s)) (depcollect (caddr s))))))

(push ‘(,x ,dep ,@s) *temp-cache*) x) ; (4)

(1)式のコードを生成する前に*temp-cache*を探索し,同じ式を保持しているテンポ

ラリがあればコードは生成せずそのテンポラリを返す.(2)見つからなければ,テンポ

ラリを生成して値を計算するコードを生成する.(3)そして,この値が依存している変

数 (derefでは第 1オペランド,四則演算では第 1および第 2オペランド)を集めてき

て,(4)連想リストを組み立て*temp-cache*に追加する.depcollectは次の通り.

(defun depcollect (x &aux a o)

(if (setq a (assoc x *temp-cache*)) (setq o (cadr a))) ; (1)

(if (symbolp x) (push x o)) ; (2)

o)

(1)オペランドが*temp-cache*に情報が保持されているならそこに保持されている依

存変数リストをもってくる.(2)このオペランドが変数であれば,その変数そのものも

依存リストに加える.

配列参照の場合,たとえば a[i]の値をテンポラリに保持していて a[j]への代入が

あった場合,テンポラリの値が無効であるかどうかは iと jが等しいかどうかに依存す

る.そこまで情報を追跡することもできなくはないが,ここでは配列の参照と代入が

中間コードでは間接参照と間接代入になることを利用し,「間接代入があった場合には,

全ての間接参照の結果は無効にする」という単純な方策を取る.このため,derefで

は依存リストに*という特別な名前を付け加える.これによってポインタ変数の参照/

代入も安全に処理される.あとは,代入の際にコード生成と並行して*temp-cache*の

Page 212: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

212 第 9章 中間コード生成

(MOV 1 NOTYET) (ADD T4 T3 T5) (MOV 1 NOTYET)

L1 (DEREF T5 T6) L6

(IFEQ NOTYET 0 L2) (ADD I 1 T7) L5

(MOV 0 NOTYET) (SUB T7 1 T8) (ADD I 1 T12)

(MOV 1 I) (MUL T8 4 T9) (MOV T12 I)

L3 (ADD T4 T9 T10) (JMP L3)

(SUB N 1 T1) (DEREF T10 T11) L4

(IFGT I T1 L4) (IFLE T6 T11 L6) (JMP L1)

(SUB I 1 T2) (MOV T6 X) L2

(MUL T2 4 T3) (MOV* T11 T5)

(ADDR A T4) (MOV* X T10)

図 9.8: 共通式最適化後の中間コード

中からその代入によって変化する変数に依存したエントリを取り除くだけである.こ

のため assignのコードを次のように修正する.

(defun assign (kind l r &aux l1 r1)

(cond ((eq (car l) ’var)

(setq r1 (gen r ’value)) (emit ‘(mov ,r1 ,(cadr l)))

(remove-cache (cadr l)))

(t

(setq r1 (gen r ’value)) (setq l1 (gen l ’addr))

(emit ‘(mov* ,r1 ,l1)) (remove-cache ’*))))

実際に依存エントリを取り除く関数 remove-chacheは次の通り.

(defun remove-cache (s)

(setq *temp-cache*

(remove-if #’(lambda (x) (member s (cadr x))) *temp-cache*)))

以上のように改良した版による生成コードは図 9.8のようになる.この程度の簡単

な値の再利用でもかなりコードを簡潔にする効果があることがわかる.

9.4 練 習 問 題

9-1. 自分の身の周りの言語処理系で,機械語に対応しないレベルのコードを出力する

ものを探し,それがどのようなコードで,なぜそうなっているのかを探求せよ.

Page 213: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

9.4. 練 習 問 題 213

9-2. 9.2.3項に示したような後置コードのインタプリタを自分の好きな言語で作成し

てみよ (例えば Lispでつくればとても簡単である).入出力や分岐などの命令が

必要だと思ったら適宜追加してよい.

9-3. 問題 9-2のインタプリタに関数呼出し命令と戻り命令を追加して動かせ.

9-4. 問題 5-4でつくったプログラムを修正して,問題 9-2(または 9-3)のインタプリタ

用後置コードを出力するようにしてみよ.

9-5. 問題 5-4でつくったソースプログラムを読み抽象構文木の S式表現を出力する

ツールに問題 7-4で行ったような型宣言と型検査を組み込み,9.3節で示したコー

ド生成の入力となるような修飾された木を出力するようにしてみよ.

9-6. 9.3.3項で示した論理式の飛越しコード生成を改良して,論理式の形がどのよう

であってもできるだけ短い飛越しコードが出るように直してみよ.

Page 214: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス
Page 215: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

215

第10章 最  適  化

前章に示した方法をそのまま適用して中間コードの代りに「とりあえず動く」アセン

ブリコードを生成するのは難しくない.しかし今日のコンパイラは「単に動くコード」

ではなく「効率のよいコード」を生成することが暗黙のうちに求められており,その

ためには最適化フェーズを経ることが必要である.本章では最適化に必要な情報の抽

出,および目的機械に比較的依存しないような最適化について述べる.

10.1 最適化の原理と分類

最適化すなわち「コードの改良」について考えるためには,「どんな基準において改良

したいか」を定める必要がある.一般には基準として (a)コードの小ささを目標にす

る,(b)コードの実行速度を目標にする,のどちらかを用いる.計算機の主記憶容量が

小さかった時期にはコードの小ささは重要であったが,現在では目的コードが占める

主記憶容量はさほど問題とされず,主として実行速度の向上が目標とされる.ここで

重要なことは,最適化によってプログラムが正しく動作しなくなってはいけない,と

いう基本原則である.つまり,ある変更が 99%の場合プログラムの実行結果を変えな

いが,残りの 1%で正しくない実行結果をもたらすなら,その変更は実施できない.こ

のことは,最適化のための情報抽出のあり方にも影響する.

では,最適化によって具体的にどのようにして実行速度を高めることが可能なのだ

ろうか.基本的な原理としては次のものがあげられる.

(a) 実行しなくてもよい命令列を発見し,取り除くこと.

(b) 複数回実行されるがその間で結果が変らない命令列を発見し,1回の実行ですま

せるように変更すること.

Page 216: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

216 第 10章 最  適  化

(c) 複数回実行される命令列を,より少ない実行回数ですませること.

(d) ある命令列を,それと同じ結果をもたらすより高速な命令列に取り替えること.

(a)と (b)は言い換えれば静的/動的に重複した計算を発見して削除することである.(c)

はループ 1展開などが相当する (つまり,ループ本体を 2 回重複させれば,ループの周

回数は半分ですむためカウンタの更新や判定命令の実行回数も半分にできる).(d)は

2n倍するのに乗算命令ではなくシフト命令を使ったり,各命令の実行時間を加味した

命令選択などが相当する.上の例からもわかるように,最適化手法とは多くの先人が

発見してきた各種の方策の「よせ集め」であり,系統的にこれだけやれば理論的に十

分,というものはない.一方,それらの手法を適用するためにはコードからどのよう

な情報を抽出すべきかについては標準的な共通の枠組が存在する.

以下ではまず手続き内最適化 (intraprocedural optimization,1 つの手続き内で閉じ

た最適化のこと)を取り上げ,最適化のための情報の抽出について述べる.続いて最適

化の各種手法について,一般の最適化とループ最適化に分けてそれぞれ解説する.最

後に手続き間最適化 (interprocedual optimization,複数の手続きにまたがって最適化

を行うこと)についても概要を説明する.中間コードの表現としては 4つ組を用いる

が,原理的には他の形式の中間コードでも同様である.情報の抽出や最適化手順のい

くつかは Lispコードとして示した 2.

10.2 最適化のためのコード解析

10.2.1 基本ブロックとフローグラフ

基本ブロック (basic block,以下単にブロックと記す)とは,途中への飛込みも途中か

らの飛出しも存在しないような命令列を意味する.具体的には,ブロックの先頭は手

続きの先頭命令,ラベル,分岐命令の直後の命令のいずれかであり,ブロックの最後

の命令はラベルの直前の命令と分岐命令のいずれかである.以下では扱いの統一のた

め,ラベルで始まらないブロックの先頭にもラベルを付加し,ブロックを同定するの

1本書ではループという語を中間コードおよび目的コード中で繰返し実行される範囲を表すのに用いる.ループは概ねソースコードの繰返し構文に対応しているが,goto とラベルによってつくられる場合もある.

2最適化の手法は非常に多く,実装が複雑なものもあるので,全てについて Lisp コードを掲げるのは実際的でない.ここでは実際に動かして見ることで原理が理解しやすくなると思われるものを中心に選んだ.

Page 217: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.2. 最適化のためのコード解析 217

に先頭のラベルを用いる.基本ブロックの重要な性質は,そこに含まれる命令列は必

ず最初から最後まで順に実行されるということである.したがって,例えばブロック

中で同じ式を複数回計算していて,なおかつその間にその式の値を変更する可能性を

もつ命令がないなら,最初に計算した値を保存しておいて繰り返し利用してもよい.

このように,各ブロック内に範囲を限って最適化を行うことを局所最適化 (local op-

timization)とよぶ.これに対し,1つの手続き内でブロックをまたがって最適化を行

うことを広域最適化 (global optimization)とよぶ 3.局所最適化のみで行える改良は

(その分手間はかかるが)全て広域最適化にも適用できるので,以下では両者の区別を

せず,広域最適化を前提として話を進める.

あるブロックを実行した後実行が進む先は,そのブロックの末尾が無条件分岐命令

ならばその飛び先,条件分岐ならば飛び先と後続ブロックの双方,それ以外では後続

ブロックのみである.1つの手続きについて,ブロックを節,行き先関係を矢線で表し

た有向グラフをその手続きのフローグラフ (flow graph)とよぶ.広域最適化を行う場

合にはフローグラフを構成し,その上で各種解析と最適化を行う.これを Lisp上で実

現するため,ブロックを B1,B2,…の記号で表し,次の広域変数を使用する.

*blocks* 全てのブロックのリスト*block-count* 生成されたブロック数の累計*block-start*,*block-end* 手続きの入口/出口ブロック

各ブロックにはそのブロックに含まれる命令列,論理的に前/後に隣接しているブ

ロック,および物理的な後続ブロックの情報を属性としてもたせる.

(defmacro block-code (b) ‘(get ,b ’code)) ; コード(defmacro block-succ (b) ‘(get ,b ’succ)) ; 後に隣接(defmacro block-prev (b) ‘(get ,b ’prev)) ; 前に隣接(defmacro block-follow (b) ‘(get ,b ’follow)) ; 後続ブロック

新しくブロックを生成する関数 newblockと,2つのブロックの間に前後関係を設定

する関数 linkblocksを示しておく.

(defun newblock (&aux b)

(setq b (newsym "B" (incf *block-count*)))

(setf (block-code b) (list b)) (setf (block-follow b) nil)

39 章末で説明した共通式計算の削除は概ね局所最適化に対応しているが,ただし条件分岐を越えて共通式計算の削除を行うので,一部はブロックをまたがっている.

Page 218: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

218 第 10章 最  適  化

(setf (block-succ b) nil) (setf (block-prev b) nil) (push b *blocks*) b)

(defun linkblocks (b1 b2)

(push b2 (block-succ b1)) (push b1 (block-prev b2)))

そして,中間コードの列 (のリスト)を受け取り,それをブロックに分けた後フロー

グラフを構成する関数 makeblocksは次の通り.

(defun makeblocks (icode &aux cur prev top dest stat code lmap)

(setq *blocks* nil) (setq *block-count* 0) ; (1)

(setq cur (newblock)) (setq *block-start* cur) (setq top t) ; (2)

(for stat icode ; (3)

(cond

((and (atom stat) top) ; (4)

(push (cons stat cur) lmap))

((atom stat) ; (5)

(setq cur (newblock)) (setq top t) (push (cons stat cur) lmap))

((member (car stat) ; (6)

’(jmp call ifeq ifne ifgt ifle ifge iflt))

(push stat (block-code cur)) (setq cur (newblock)) (setq top t))

(t ; (7)

(push stat (block-code cur)) (setq top nil))))

(setq *block-end* cur)

(setq prev (car *blocks*)) ; (8)

(for b (cdr *blocks*) (setf (block-follow b) prev) (setq prev b))

(setq *blocks* (reverse *blocks*))

(for b *blocks* ; (9)

(setq code (block-code b)) (setq stat (car code))

(cond

((atom stat) ; (10)

(if (setq dest (block-follow b)) (linkblocks b dest)))

((eq (car stat) ’jmp) ; (11)

(setq dest (cdr (assoc (cadr stat) lmap))) (linkblocks b dest)

(setq stat (list ’jmp dest))

(setq code (cons stat (cdr code))))

((member (car stat) ’(ifeq ifne ifgt ifle ifge iflt)) ; (12)

(setq dest (cdr (assoc (cadddr stat) lmap))) (linkblocks b dest)

(setq stat (list (car stat) (cadr stat) (caddr stat) dest))

(if (setq dest (block-follow b)) (linkblocks b dest))

(setq code (cons stat (cdr code))))

(t ; (13)

(if (setq dest (block-follow b)) (linkblocks b dest))))

Page 219: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.2. 最適化のためのコード解析 219

(setf (block-code b) (reverse code)))) ; (14)

(1)広域変数を初期設定した後 (2)先頭のブロックを生成し,「ブロックの頭である」と

いう旗を立てておく.(3)次にコードを先頭から順に走査し,(4)ラベルでありかつブ

ロックの頭ならブロック名とラベルの対照表 lmapに対応関係を登録する.(5)ブロッ

クの中途でラベルがあったら,新しいブロックを始めた後先と同様にする.(6)分岐命

令等の場合はその命令を現在のブロックに付加した後,新しいブロックを始める.(7)

それ以外の命令なら現在のブロックに付加した後,旗を降ろしておく.これでブロック

は一通りできて,*blocks*にブロックのリストが逆順に入っている状態となった.こ

こで,(8)末尾のブロックを除く各ブロックについて,後続ブロックを属性 followに

記憶させておく.(9)その後逆順を正順に直して,再び各ブロックについて,そのコー

ドの末尾の命令を調べていく.(10)もしそれがラベルであれば空のブロックだから後

続ブロックとリンクする.(11)無条件分岐ならその行き先ラベルをブロック名に書き

換え,このブロックと行き先をリンクする.(12)条件分岐であれば同様だが,後続ブ

ロックともリンクする.(13)それ以外なら単に後続ブロックとリンクする.(14)最後

にコードも逆順を正順に直しておく.

図 9.8の中間コード (をリストとして並べたもの) が変数*icode*に入っているとし

て,これを実行させた様子を示す.

>(makeblocks *icode*)

NIL

>(for b *blocks* (format t "~% ~A ~A" (block-code b) (block-succ b)))

(B1 (MOV 1 NOTYET)) (B2)

(B2 (IFEQ NOTYET 0 B9)) (B3 B9)

(B3 (MOV 0 NOTYET) (MOV 1 I)) (B4)

(B4 (SUB N 1 T1) (IFGT I T1 B8)) (B5 B8)

(B5 (SUB I 1 T2) (MUL T2 4 T3) (ADDR A 0 T4) (ADD T4 T3 T5)

(DEREF T5 T6) (ADD I 1 T7) (SUB T7 1 T8) (MUL T8 4 T9)

(ADD T4 T9 T10) (DEREF T10 T11) (IFLE T6 T11 B7)) (B6 B7)

(B6 (MOV T6 X) (MOV* T11 T5) (MOV* X T10) (MOV 1 NOTYET)) (B7)

(B7 (ADD I 1 T12) (MOV T12 I) (JMP B4)) (B4)

(B8 (JMP B2)) (B2)

(B9) NIL

なお,番地を取る中間命令 addrのオペランドが 1つ増えているが,これは「変数X

のN 番地先の番地をとる」ことができるように直したもので,後でこれを最適化に用

Page 220: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

220 第 10章 最  適  化

B1

B2

B3

B4

B5

B7

B6

B8

B9

図 10.1: バブルソートの手続きのフローグラフ

いる.ブロックの接続関係をフローグラフに描いたものを図 10.1に示しておく.

10.2.2 制御フロー解析

フローグラフを作成した後まず制御フロー解析 (control flow analysis)を行うが,そ

の主要な目的はプログラム中のループを同定することである.多くのループはソース

コードの繰返し構文に対応しているのだから,フローグラフからループを再発見する

代りに繰返し構文の情報を保持しておく方法もある.ここでは gotoなどによるループ

も同定できる等,より一般的性質をもつフローグラフからのループ同定について述べ

る.まず,いくつかの用語を定義する.

定義 フローグラフGに含まれる節 dが節 nを支配する (dominate)とは,グラフの出

発点から nに至る全ての経路が dを通ることをいう.

定義 フローグラフGに含まれる辺 b → hが帰辺 (back edge)であるとは,hが bを支

配する節である場合をいう.

定義 フローグラフGに含まれる帰辺 b → hに関する自然ループ (natural loop)とは,

nから bへ hを通らずに行ける経路があるような節 n(b自身を含む)と hを合せ

たものである.ここで hをループのヘッダ (header)とよぶ.

Page 221: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.2. 最適化のためのコード解析 221

では,先のデータ構造をもとに自然ループを求めてみる.次の広域変数を使用する.

*paths* 出発点からの全ての経路のリスト*back-edges* 帰辺のリスト*nat-loops* 自然ループのリスト*natloop-hdrs* 自然ループのヘッダのリスト

まず,支配する節を判定するため,グラフの出発点から始まる,ループを含まない

全ての経路で,他の同様な経路の部分経路になっていないものの集合を求めておく.

(defun findallpaths ()

(setq *paths* nil) (findpath *block-start* (list *block-start*)))

(defun findpath (n1 path &aux more)

(for n2 (block-succ n1)

(if (not (member n2 path))

(progn (findpath n2 (cons n2 path)) (setq more t))))

(if (not more) (push path *paths*)))

findpathは指定された節とそこまでの経路を元に,その経路に含まれていない節へ

の辺を全て,自分自身を再帰的に呼び出すことによってたどる.そのような節がなけ

ればこの経路はこれ以上伸ばせないので*paths*に登録する.findallpathsはグラフ

の出発点を指定して findpathを呼び出す.先のグラフに対しての実行例を示す.

>(findallpaths)

NIL

>*paths*

((B9 B2 B1) (B8 B4 B3 B2 B1) (B7 B5 B4 B3 B2 B1)

(B7 B6 B5 B4 B3 B2 B1))

このプログラムでは経路の逆順の集合が求まるが,その方があとで都合がよい.こ

の情報を参照すれば,支配する節かどうかの判定は次のように行える.

(defun dominates (n1 n2 &aux m1 (dom t))

(for p *paths*

(setq m2 (member n2 p))

(if (and m2 (not (member n1 m2))) (setq dom nil)))

dom)

memberはその第 1引数が第 2引数のリスト中に存在すればそこから後の部分リスト

を返すので,*paths*の各要素について用いた場合には出発点から指定した節までの

Page 222: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

222 第 10章 最  適  化

経路 (の逆順)が求まる.n1が n2を支配しているかどうか調べるには,全経路のうち

n2を含んでいるもの全てについて,出発点からそこまでの途中に n1が含まれている

か調べればよい.1つでも含まれていない場合があれば支配していないことになる.

次に,n1から n2へ n3を通らずにいけるかどうか調べる関数 reachwithoutを示す.

引数 pathはここまでに通った節のリストで,最初は nilを渡せばよい.

(defun reachwithout (n1 n2 n3 path &aux)

(cond ((eq n1 n3) nil) ; (1)

((eq n1 n2) t) ; (2)

(t (for n4 (block-succ n1) ; (3)

(cond ((eq n4 n3))

((member n4 path))

((reachwithout n4 n2 n3 (cons n1 path))

(return-from reachwithout t)))))

nil)) ; (4)

(1)n3を通ってしまったらだめ,(2)そうでなくて n2に到達していたらOK,(3)そう

でなければまだ目的地でないので,n1から出ている全ての辺について,それが n3でも

すでに通った節でもなく,そこから n2に n3を通らずいける場合が 1つでもあるなら

OK,(4)そうでなければだめである.これを用いて自然ループを探す関数 findloops

は次の通り.

(defun findloops (&aux h n b nloop)

(for b *blocks*

(for h (block-succ b)

(if (dominates h b) (push (list b h) *back-edges*))))

(for e *back-edges*

(setq h (cadr e)) (setq b (car e)) (setq nloop nil)

(for n *blocks*

(if (reachwithout n b h nil) (push n nloop)))

(push h nloop) (push nloop *nat-loops*)))

すなわち,全ての節から出ている全ての辺について,支配関係を見ることで帰辺か

どうかを調べて帰辺を*back-edges*に集め,続いて帰辺ごとに全ての節 nについてそ

れがループ中の節かどうかを調べてループに含まれる節を求める.実行例は次の通り.

>(findloops)

NIL

Page 223: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.2. 最適化のためのコード解析 223

B1

B2

B3

B4

B1

B2

B3

B4 B5

図 10.2: 自然でないループとヘッダを共有するループ

>*back-edges*

((B8 B2) (B7 B4))

>*nat-loops*

((B4 B7 B6 B5) (B2 B8 B7 B6 B5 B4 B3))

確かに,先のプログラムのループが正しく求まっている.

ところで「自然な」ループがあるからには「自然でない」ループも存在する.具体

的には,自然でないループとは図 10.2左のように入口が 2つ以上あるものを意味する.

そのため,後で述べるようなループ最適化のための変形が行いにくい.一方,このよ

うなループは普通の言語で繰返し構文を使用している分には現れないし,gotoを用い

た場合でも実際には稀であるとされる.そのため自然でないループはループ最適化の

対象としないという選択が一般的である.

また,図 10.2右のように複数の帰辺が共通の行き先 hをもつ場合には部分的にオー

バラップした複数の自然なループができる.これも最適化のために不都合なので,新し

い空な節 bと帰辺 b → h をグラフに追加し,これまでの帰辺の行き先をこの bに変更

する.これと合せて,後での最適化の便宜のため,新しい空っぽの節 h0(プリヘッダ—

pre-header)と辺 h0 → hをGに加え,ループ外を起点とする辺でこれまで hを行き先

としていたものの行き先を全て h0に変更する (プログラム例は略した).これらの変更

により,修正されたGについて次の性質が成り立つ.

• 各自然ループは 1つだけの入口点 (つまりヘッダ)とそこへ到達する 1つだけの

節 (プリヘッダ)と 1つだけの帰辺をもつ.

• 2つの自然ループの関係は,互いに共通の節をもたないか,一方が他方に完全に

含まれるかのいずれかである.

Page 224: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

224 第 10章 最  適  化

10.2.3 データフロー解析

最適化のためには「この変数の値はこの後参照されるだろうか.」(されないなら,そ

の変数に値を格納しておく命令は不要である),「この場所でこの変数に入っている値

を設定する命令はどれとどれだろうか.」(それが 1つだけで,その値がたまたま別の

変数にも入っているなら実は変数そのものが要らないかもしれない),などの情報が必

要である.この種の情報をフローグラフから抽出するのがデータフロー解析 (dataflow

analysis)である.以下ではまず解析のためのデータ構造について準備した後,いくつ

かの主要なデータフロー問題について述べる.

a. データフロー解析のためのデータ構造  これ以降の解析に当たっては,「ど

の文」までを考慮する必要があるので,前節までのデータ構造をさらに変更し,ブロッ

ク内の各文に S1,S2,…のように固有の名前をつけ,実際のコードはその code属性

に入れておくようにする.そしてブロックの code 属性には (ブロック先頭のラベルを

除いた)各文の名前のリストを保持させることにする.合せて,各文ごとにその文で値

を設定する変数の集合,値を参照する変数の集合,間接代入/参照の有無を属性として

もたせるものとする.

(defmacro stat-code (s) ‘(get ,s ’code)) ; 命令本体(defmacro stat-def (s) ‘(get ,s ’def)) ; 設定する変数(defmacro stat-ref (s) ‘(get ,s ’ref)) ; 参照する変数(defmacro stat-suspdef (s) ‘(get ,s ’suspdef)) ; 間接代入(defmacro stat-suspref (s) ‘(get ,s ’suspref)) ; 間接参照

関数newstatは新しい文を1つ生成し,各種属性をnilに初期設定する.変数*stats*

に全ての文のリスト,*stat-count*にその番号の累計を入れるものとする.

(defun newstat (&aux s)

(setq s (newsym "S" (incf *stat-count*)))

(setf (stat-def s) nil) (setf (stat-ref s) nil)

(setf (stat-suspdef s) nil) (setf (stat-suspref s) nil)

(push s *stats*) s)

関数 pushref/pushdefは変数を参照/設定属性に追加する.pushrefで記号かどう

か調べているのは演算のオペランドが数値のときは追加しないためである.

(defun pushref (v s) (if (symbolp v) (push v (stat-ref s))))

(defun pushdef (v s) (push v (stat-def s)))

Page 225: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.2. 最適化のためのコード解析 225

B1 B5 B6

S1: (MOV 1 NOTYET) S7: (SUB I 1 T2) S18: (MOV T6 X)

B10 S8: (MUL T2 4 T3) S19: (MOV* T11 T5)

B2 S9: (ADDR A 0 T4) S20: (MOV* X T10)

S2: (IFEQ NOTYET 0 B9) S10: (ADD T4 T3 T5) S21: (MOV 1 NOTYET)

B3 S11: (DEREF T5 T6) B7

S3: (MOV 0 NOTYET) S12: (ADD I 1 T7) S22: (ADD I 1 T12)

S4: (MOV 1 I) S13: (SUB T7 1 T8) S23: (MOV T12 I)

B11 S14: (MUL T8 4 T9) S24: (JMP B4)

B4 S15: (ADD T4 T9 T10) B8

S5: (SUB N 1 T1) S16: (DEREF T10 T11) S25: (JMP B2)

S6: (IFGT I T1 B8) S17: (IFLE T6 T11 B7) B9

図 10.3: 文ごとに名前づけされた中間コード列

前述のようにデータ構造を変換する関数 convgraphは次の通り.

(defun convgraph (&aux code)

(for b *blocks* ; (1)

(setq code nil)

(for c (cdr (block-code b)) ; (2)

(setq s (newstat)) (setf (stat-code s) c) (push s code)

(case (car c) ; (3)

((mov) (pushref (cadr c) s) (pushdef (caddr c) s))

((mov*) (pushref (cadr c) s) (setf (stat-suspdef s) t))

((addr) (pushdef (cadddr c) s))

((deref) (pushref (cadr c) s) (pushdef (caddr c) s)

(setf (stat-suspref s) t))

((add sub mul div)

(pushref (cadr c) s) (pushref (caddr c) s)

(pushdef (cadddr c) s))

((ifeq ifne ifgt ifle ifge iflt)

(pushref (cadr c) s) (pushref (caddr c) s))))

(setf (block-code b) (reverse code))))

(1)各ブロックについて (2)その各文を生成し,(3)命令の種類に応じて文の各属性を

設定する.以下の参照に都合がよいように,各命令をブロック名,文名とともに表示

したものを図 10.3に示す.プリヘッダ B10,B11が加わったことにも注意.

b. 生きている変数の問題  データフロー解析の最初の例として,先にあげた

Page 226: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

226 第 10章 最  適  化

「この変数の値はこの後参照されるだろうか.」という問題を考える.これは変数が生

きている (live),つまりその変数の値が後で参照される可能性があるか,あるいは死ん

でいる (dead),つまりその可能性がないか,を決めるものである.

個々のブロック bについて見ると,その入口で生きている変数と出口で生きている

変数の間には次の関係がある.

LiveOut[b] =⋃

b′∈Succ[b]

LiveIn[b′]

LiveIn[b] = Ref [b] ∪ (LiveOut[b]−Ass[b])

これは bの出口で生きている変数の集合とは bに引き続くようなブロック b′の入口で

生きている変数の和集合であり,また bの入口で生きている変数の集合は出口で生き

ている変数の集合から bで値を設定してしまう変数は除き,代りに bで (もし値を設定

するならそれより前に)参照する変数を加えたものであることを表す.

次にフローグラフ Gに関してこの方程式 (データフロー方程式)を解く必要がある.

それには次の手順による.

1. まず,各ブロックについて Ref [b],Ass[b]を求める.

2. LiveIn[b],LiveOut[b]についてはとりあえず空集合とする.

3. 各ブロックについて上の方程式に従ってLiveIn[b],LiveOut[b]を計算すること

を反復することをもはや各集合が変化しなくなるまで行う.

各反復において,LiveIn[b]と LiveOut[b]は (変化するとすれば)要素がつけ加わる方

向にのみ変化し,そして変数の個数は有限だから,この手順は必ず停止し解が求まる.

実際にこれを実現するコードを示す.まず,各ブロック bの Ref [b],Ass[b]を求め,

これを各ブロックの次の 2つの属性に格納する.

(defmacro block-ass (b) ‘(get ,b ’ass))

(defmacro block-ref (b) ‘(get ,b ’ref))

配列等を正しく扱うため,9章と同様,アドレスを取った変数を全て「疑わしい」も

のとして覚えておき,間接代入はそれら全てを変更する可能性をもち,間接参照はそ

れら全てを参照しているとみなす.そのため,まず関数 collect-suspにより疑わし

い変数の集合を広域変数*susp-vars*に求める.

Page 227: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.2. 最適化のためのコード解析 227

(defun collect-susp ()

(for b *blocks*

(for s (block-code b)

(setq c (stat-code s))

(if (eq (car c) ’addr) (pushnew (cadr c) *susp-vars*)))))

次にこの情報も参照しながら,ブロックごとに Ref [b],Ass[b]を求める.

(defun collect-refass (&aux ass ref susp)

(for b *blocks*

(setq ass nil) (setq ref nil) (setq susp nil)

(for s (block-code b) ; (1)

(for v (stat-ref s) ; (2)

(if (not (member v ass)) (pushnew v ref))) ; (3)

(if (stat-suspref s) (setq susp t)) ; (4)

(for v (stat-def s) (pushnew v ass))) ; (5)

(if susp (setq ref (union *susp-vars* ref))) ; (6)

(setf (block-ref b) ref) (setf (block-ass b) ass)))

(1)各ブロックの頭から 1文ずつ走査し,(2)その文が参照している各変数について,

(3)それがこのブロックで値を設定されたのでないならば Ref に加え,(4)間接参照が

あるなら旗 suspを立て,(5)この文で値を設定している変数を Assに加え,(6)もし

旗 suspが立っていれば疑わしい変数全てもRef に加える.

以上で準備ができたので,データフロー方程式を解く方に進む.再び,各ブロック

ごとに属性 livein,liveoutを用意し,その初期値は nilにしておくものとする.方

程式を解く関数 comp-livenessは次の通り.

(defun comp-liveness (params &aux lo li (notyet t))

(collect-susp) (collect-refass)

(setf (block-livein *block-start*) params) ; (1)

(while notyet ; (2)

(setq notyet nil)

(for b *blocks*

(setq lo nil) (setq li nil)

(for b1 (block-succ b) ; (3)

(setq lo (union lo (block-livein b1))))

(if (< (length (block-liveout b)) (length lo)) ; (4)

(progn (setf (block-liveout b) lo) (setq notyet t)))

(setq li (union (block-ref b) ; (5)

(set-difference lo (block-ass b))))

Page 228: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

228 第 10章 最  適  化

(if (< (length (block-livein b)) (length li)) ; (6)

(progn (setf (block-livein b) li) (setq notyet t))))))

(1)手続き最初のブロックについては,その入口では手続きに渡される引数が生きてい

るので,それを引数として受けとってLiveInの初期値とする.(2)あとは状態の変化

があったかどうかの旗が立っている限り以下の反復計算を行う.(3)LiveOutはこのブ

ロックの後続ブロック全てのLiveInを合せたものとし (方程式通り),(4)それがこれ

までの途中結果と違っていれば,旗を立てる.(5)LiveInも方程式通り計算し,(6)変

化していたら旗を立てる.実行例を示す.

>(comp-liveness ’(a n))

NIL

>(for b *blocks*

(format t "~% ~A ~A ~A" b (block-livein b) (block-liveout b)))

B1 (A N) (NOTYET N A)

B10 (NOTYET N A) (NOTYET N A)

B2 (NOTYET N A) (N A)

B3 (N A) (N A I NOTYET)

B11 (N A I NOTYET) (N A I NOTYET)

B4 (N A I NOTYET) (A I N NOTYET)

B5 (I N A NOTYET) (T10 T11 T5 T6 I N A NOTYET)

B6 (T10 T11 T5 T6 I N A) (I N A NOTYET)

B7 (I N A NOTYET) (N A I NOTYET)

B8 (NOTYET N A) (NOTYET N A)

B9 NIL NIL

これを見ると,変数 iは forループの中だけで生きており,また notyetへの無条件

代入を含むブロックの入口では notyetは生きていないことがわかる.なお,ここでは

反復計算の順序については特に配慮しなかったが,実用的にはブロックの出口の値を

もとに入口の値が求まるので,なるべく手続きの開始点から遠いブロックから前に向

かって順序評価していく方が効率がよい.このような性質をもつデータフロー解析を

後ろ向きフロー解析 (backward flow analysis)とよぶ.

c. UD-連鎖の問題  続いて「この場所でこの変数に入っている値を設定する

命令はどれとどれか.」の問題を扱う.その前にいくつか用語を説明しておく.

定義 変数 xへの定義 (definition)とは,変数に値を設定する可能性をもつ命令を言う.

典型的には xへの代入がそうだが,xを参照渡し引数とする手続き呼出しは xを

Page 229: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.2. 最適化のためのコード解析 229

更新する可能性があるため xの定義となる.xに必ず値が設定される場合にはそ

の定義は曖昧でない (unambiguous)という.

定義 変数 xに対する定義 dと命令 sについて,dから s へ至る経路上に xに対する別

の曖昧でない定義 d′ があるとき,d′ は dを殺す (kill)という.言い換えれば d′

によってその経路を通るときは sへの dの影響が及ばなくなる.

定義 定義 dと命令 sについて,dを殺す別の定義が存在しないような dから sへの経

路が 1つ以上存在するとき,dは sに到達 (reach)する,という.

以上の用語から,この問題は到達する定義 (reaching definition)の問題,または変数を

使う (use)場所ごとに,それに影響し得る定義 (definition)の連鎖を求めるためUD-

連鎖 (UD-chain)の問題とよばれる.この問題については,個々のブロック bについて

次のデータフロー方程式が成り立つ.

DefIn[b] =⋃

b′∈Pred[b]

DefOut[b′]

DefOut[b] = Def [b] ∪ (DefIn[b]−Kill[b])

すなわち,ブロック bに入ってくる定義の集合はそのブロックの上流である全ブロッ

クから出てくる定義を全部合せたものであり,また bから出ていく定義は bにおける

定義と,入ってきた定義のうち b で殺されないものとを合せたものとなる.

このデータフロー方程式は先の「生きている変数」の式と In/Outが逆になった形を

している.したがって解を求める手順は前と同様でよいが,実用的には手続きの入口

点から先に向かって反復計算すると効率がよい.このような問題を前向きフロー解析

(forward flow analysis)の問題とよぶ.

d. DU-連鎖の問題   UD-連鎖が「各参照ごとに,そこに到達し得る定義を求

める」のに対し,その逆,つまり「各定義ごとに,それに到達され得る参照を求める」

問題をDU-連鎖 (DU-chain)の問題とよぶ.これを求めるデータフロー方程式は次の

ようになる.

UseOut[b] =⋃

b′∈Succ[b]

UseIn[b′]

UseIn[b] = ExposedRef [b] ∪ (UseOut[b]− UseKill[b])

Page 230: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

230 第 10章 最  適  化

ただしExposedRef [b]は bの中で定義されないか,または定義されてもそれより前の

位置で値を参照する文の集合,UseKill[b]は bで定義される値を参照する b以外の場

所にある文の集合を意味する.各集合の意味は違っても,方程式の形は最初に述べた

生きている変数の問題と同じである.

e. 共通部分式の問題  ここまでに示したデータフロー方程式はいずれも上流/

下流のどこかで問題としている事象 (値の定義とか変数の参照)が起き得るかどうかを

知るためのものであり,その主旨からいずれかの経路解析 (any-path analysis)とよば

れる.データフロー解析にはそれと対をなす全ての経路解析 (all-path analysis)も存在

する.その例として,ブロックにまたがる共通部分式の最適化を考える.コード上のあ

る場所に出てきた式の値を改めて計算しないですむためには,その上流の全ての経路で

その式の値が計算ずみである必要がある.この問題を利用可能式 (available expression)

の問題とよび,次のデータフロー方程式で表される.

AvailIn[b] =⋂

b′∈Pred[b]

AvailOut[b′]

AvailOut[b] = DefExp[b] ∪ (AvailIn[b]−KillExp[b])

ここでDefExp[b]は bで計算される式の集合,KillExp[b]は bにおいて式に含まれる

変数のどれかが殺されるため計算ずみの値が有効でなくなる式の集合を意味する.こ

の方程式もこれまでと同様の解法で扱うことができるが,ただし計算が⋂を用いて集

合を小さくする方向に進むので,AvailIn[b],AvailOut[b]の初期値は初期ブロックの

み空集合,あとは全ての式の集合とする.

f. コピー文の問題  利用可能式と類似した問題にコピー文 (copy statement)

の問題がある.これは例えばある場所に y := xという代入 (コピー文)があり,後続

するブロックに yの参照があったとき,これを xに置き換えてしまってよいかどうか

を決めるものである.置換えが可能なためには

• yを参照している箇所に到達する全ての yに対する定義が y := xである.

• y := xが行われた後 yの参照までに,xの値を変更する可能性がない.

の 2つの条件を満たす必要がある.このうち前者はUD-連鎖の情報をもとに知ること

Page 231: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.3. 一般の各種最適化 231

ができ,後者は次のデータフロー方程式により計算できる.

CopyIn[b] =⋂

b′∈Pred[b]

CopyOut[b′]

CopyOut[b] = DefCopy[b] ∪ (CopyIn[b]−KillCopy[b])

ここで CopyIn[b]/CopyOut[b]はそれぞれ bの入口/出口で置き換え可能なコピー文の

集合,DefCopy[b]は bに含まれるコピー文でその後 b内で左辺,右辺どちらも変更さ

れないものの集合,KillCopy[b] は b以外の場所にある全てのコピー文から b内で左

辺,右辺どちらかが変更されるものを除いた集合である.

10.2.4 記号実行と範囲解析

ここまでに述べたコード解析は主としてコード上の各点間のデータの流れを調べる

ものであったが,これと直交するコード解析として,データの値そのものを解析する

ことも必要である.記号実行 (symbolic execution)とは,翻訳時にプログラムコード

を「実行」して式の値や実行経路を求めることを指す.もちろん翻訳時には入力デー

タの値などはわからないので,実行時の数値を完全に知ることは不可能であり,した

がって式の値を記号的に扱う必要がある.記号実行という呼び方はここから来ている.

ただし部分式の中に定数しか現れず,したがって式の値が完全に計算できる場合もあ

る.そのような場合には後述する定数の畳み込みが可能になる.

また,式の値そのものが確定しなくても,その上限と下限がわかる場合もある.範

囲解析 (range analysis)はこのような情報,つまりコード上の各式ごとに,その上限

と下限を見出そうとするもので,その結果を利用することで不到達コードが発見でき

たり,実行時の配列の範囲検査の削除などが可能になる.

10.3 一般の各種最適化

10.3.1 最適化の各種手法の分類

10.1節で述べたのように,最適化は各種手法の寄せ集めであるが,それらは全体として

ループ最適化 (loop optimization) — ループを特に考慮したもの — とそれ以外に分け

Page 232: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

232 第 10章 最  適  化

られる.ループ最適化は次節に譲り,本節ではそれ以外のものを扱う.基本的な考え方

としてはコード解析の情報に基づいてコードを効率のよいものに置き換え,重複計算

を避けるように変形する.以下ではまず各種手法の概要を示し,その後適用例を示す.

a. 定数の畳み込み  定数の畳み込み (constant folding)とは,翻訳時に値が決

定できる式の計算を結果の定数で置き換えるものである.そのような式の値はプログ

ラマが自ら計算してやればよいように思えるかもしれないが,記号定数などを使用し

読みやすさを優先したコーディングでは定数式の出現は一般的である.

また配列添字に定数を書いた場合,その番地計算の一部は定数式となる.例えば a[10]

という配列参照を考えてみると,各要素の大きさが 4 バイトであれば,aの先頭から

10× 4 = 40バイト先を参照することになるが,この 40という値をプログラマがあら

かじめ計算しておくことはできない.さらに配列 aの先頭アドレスは一般に base+N

で表される (baseは広域変数の場合は 0または広域変数用ベースレジスタ,局所変数で

はフレームポインタ,中間レベル変数ではディスプレイレジスタまたは静的チェイン

から求めた番地であり,N は定数となる).したがって定数N + 40をあらかじめ計算

しておき,これを baseに加えるコードを出すようにすれば,この要素アクセスは普通

の変数と同じ速度で実行できる.

また,式が直接には定数式でなくても,くくり出しによって演算回数を減らせる場合

もある.例えば先の例が a[i+10]だったとすると,その番地は Addr[a] + (i+ 10)× 4

になるが,4を分配法則で括弧中に入れて Addr[a] + i× 4 + 40となり,さらに 40を

aの番地計算に含めてAddr[a + 40] + i× 4とすることで加算を 1回節約できる.

b. 数学的等価  式の計算において,数学的に等価だが計算機で実行すると一

方が他方より高速であるような場合がある.具体的には次のようなものがあげられる.

• x× 2と x+ x.

• x2と x× x.

• x× 2n と nビット左シフト (2進数表現の機械で).

いずれも左側の形のものを右側のように置き換えることで実行時間が短縮できる.こ

れは,強い (実行時間のかかる)演算をそうでない演算に置き換えることから演算強さ

の軽減 (strength reduction)ともよばれる.また,次のような場合には演算全体を削除

したり定数で置き換えることができる.

Page 233: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.3. 一般の各種最適化 233

• x× 0 → 0

• x× 1 → x

• x+ 0 → x

c. 共通式の除去  共通式の除去は,利用可能式の情報を使用して行える.す

なわち,式の値を計算している各箇所で,その式と同じものが利用可能式の集合に含

まれているかどうか調べ,含まれていれば値の計算をすでに計算された値への参照に

置き換える.

d. コピー伝搬  コピー伝搬 (copy propagation)とは,複数の変数が同じ値を

もっているときそのうちどれか 1つだけを参照するようにコードを変更することを指

す.ある変数の値を別の変数に代入 (コピー)したときそのような状況が生じるためこ

の名前がある.

e. 不要コードの削除  不要コードの削除 (useless code elimination)とは,参

照されない式を計算しているコードを除去することを言う.これには生きている変数

解析の結果を使用する.

f. 不到達コードの削除  不到達コードの削除 (dead code elimination)とは,

定数の畳み込みなどの結果,ifや caseなどの条件式の値が翻訳時に定まるような場合,

その情報をもとに常に選択される経路のみを残し通らないコードを削除することを言

う.そのような「明らかに実行されない」枝をもつプログラムを実際に書くものだろ

うか,という疑問もあるかもしれないが,例えば

if(debuglevel > 3) ....

のように (たぶん広域)変数の値に応じて異なる詳しさの虫取り用情報表示を行うコー

ドを書き,完成したら単にこの変数の値を 0にして再翻訳するだけで,不要な虫取り

用コードは一切含まない目的コードが生成できる,というやり方が考えられる.これ

は一種の「条件付き翻訳」機能を実現しているものといえる.もっと役に立ちそうな

のは,変数の上限と下限を追跡しておくことで

if(i < lowlim | i > highlim) ...

のような範囲検査コードを削除することである.削除されるコードとしては人が手で

書く場合だけでなく,コンパイラによって配列の添字検査として自動的に埋め込まれ

る場合も含まれてよい.

Page 234: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

234 第 10章 最  適  化

A

A

A

左向きホイスティング

右向きホイスティング

A

A

A

図 10.4: ホイスティングの原理

g. ホイスティング  ホイスティング (hoisting)とは,図 10.4に示すように,

フローグラフが枝分かれした直後の両方の枝に同じコードがある場合これを 1つにま

とめて枝分かれの前に移すこと,および合流の直前に同じコードがある場合にこれを

1つにまとめて枝分かれの後に移すことを指す.前者を左向きホイスティング,後者を

右向きホイスティングとよんで区別することもある.

ホイスティングによってコードの大きさは節約できるが,直接実行速度が改良される

わけではない.ただしループ内部のように頻繁に実行される部分が目的機械のキャッ

シュメモリに入りきるかどうかで実行速度が大きく変化するような境界上では非常に

有効である可能性もある.

10.3.2 一般の最適化の例

本節では,図 9.8の中間コード列を題材とし,これまでに述べた最適化のいくつかを

施す.まず最初に,コードが計算する値を記号実行により解析し,定数式や数学的等

価などの簡略化を施す.このため,各式ないし部分式ごとに E1,E2,… のように記号

を割り当て,そのリストを変数*exprs*に保持する.各式に対しては,その演算子と

左部分式と右部分式の 3要素から成るリストを属性 bodyに保持する (単項演算子のと

きは右部分式を nilにしておく).また,式ごとにその式が依存している変数のリスト

ももたせる.

Page 235: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.3. 一般の各種最適化 235

(defmacro expr-body (e) ‘(get ,e ’body))

(defmacro expr-oper (e) ‘(car ‘(get ,e ’body)))

(defmacro expr-left (e) ‘(cadr ‘(get ,e ’body)))

(defmacro expr-right (e) ‘(cadr ‘(get ,e ’body)))

(defmacro expr-depend (e) ‘(get ,e ’depend))

ここには含めなかったが,各テンポラリごとにそのテンポラリに計算されている式

を属性 expに保持するものとし,これをアクセスするマクロ temp-expを用意した.ま

た記号がテンポラリかどうかを述語 is-tempで調べられるようにした.

式の左右の部分式に表れ得るものは式,変数,定数,nilの 4種類である.これら

が依存している変数の集合を計算する関数 collectdependを用意する.

(defun collectdepend (x)

(cond ((or (null x) (numberp x)) nil) ; (1)

((expr-body x) (expr-depend x)) ; (2)

(t (list x)))) ; (3)

(1)数や nilなら依存変数はなく,(2)式であればその depend属性を返し,(3)変数な

らその変数自身のみから成るリストを返す.なお,間接参照を含む式は全て*という特

別な変数に依存するものとしておく.

これをもとに,「(newexp 演算子 左部分式 右部分式)」のように呼び出されるとそ

のような式がすでに存在すればその記号を返し,なければ新たに記号を割り当てて返

す関数 newexpを用意する.

(defun newexp (op l r &aux b e dep)

(if (and (symbolp l) (is-temp l)) (setq l (temp-exp l))) ; (1)

(if (and (symbolp r) (is-temp r)) (setq r (temp-exp r))) ; (1)

(setq b (list op l r)) ; (2)

(for e *exprs*

(if (equal b (expr-body e)) (return-from newexp e))) ; (3)

(setq e (newsym "E" (incf *expr-count*))) (setf (expr-body e) b) ; (4)

(case op ; (5)

((addr) (setf (expr-depend e) nil))

((deref) (setf (expr-depend e) (cons ’* (collectdepend l))))

((add sub mul div)

(setf (expr-depend e) (union (collectdepend l)

(collectdepend r)))))

(push e *exprs*) e)

Page 236: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

236 第 10章 最  適  化

(1)左右の部分式がテンポラリであれば,そのテンポラリに計算されている式は属性

expに入っているので,これをもってくるが,そうでなければ変数/数値/nilをそのま

ま部分式として,(2)リストを組み立てる.(3)次に式のリスト中に同じ形の式があれ

ばそれを値として返す.(4)そうでなければ新しい記号を生成し,bodyをセットする.

(5)次に演算の種類ごとに依存する変数の集合を計算しセットする.最後にこの記号を

*exprs*に追加した後返す.コードから全ての式を抽出する手続き collect-exprを

以下に示す.

(defun collect-expr (&aux d kill)

(for b *blocks* (collect-expr-bl b))) ; (1)

(defun collect-expr-bl (b &aux def c d e)

(for s (block-code b)

(setq c (stat-code s)) ; (2)

(case (car c)

((add sub mul div) ; (3)

(setq e (newexp (car c) (cadr c) (caddr c)))

(setq r (cadddr c)) (if (is-temp r) (setf (temp-exp r) e)))

((deref) ; (4)

(setq e (newexp ’deref (cadr c) nil)) (setq r (caddr c))

(if (is-temp r) (setf (temp-exp r) e)))

((addr) ; (5)

(setq e (newexp ’addr (cadr c) (caddr c)))

(setq r (cadddr c)) (if (is-temp r) (setf (temp-exp r) e))))))

(1)collect-expr自体はブロックごとに下請け collect-expr-blを呼び出す.その中

では (2)各文の命令で分岐し,(3) 演算命令,(4)間接参照命令,(5)番地取得命令につ

いてそれぞれ newexpにより式をつくり,代入先がテンポラリならその exp属性に式

の記号を入れておく.これの実行例を示す.

>(collect-expr)

NIL

>(for e (reverse *exprs*)

(format t "~% ~A ~A ~A e (expr-body e) (expr-depend e)))

E1 (SUB N 1) (N)

E2 (SUB I 1) (I)

E3 (MUL E2 4) (I)

E4 (ADDR A 0) NIL

E5 (ADD E4 E3) (I)

E6 (DEREF E5 NIL) (* I)

Page 237: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.3. 一般の各種最適化 237

E1

N 1

E2

I 1

-

-

E3

4

*E4

A 0

addr

E5+

derefE6

E7

I 1

+

E8-

1

E9

*

4

E10+

derefE11

図 10.5: 式情報の非循環有向グラフ

E7 (ADD I 1) (I)

E8 (SUB E7 1) (I)

E9 (MUL E8 4) (I)

E10 (ADD E4 E9) (I)

E11 (DEREF E10 NIL) (* I)

実はここで行っていることは,E1,E2,… を節とし,変数や定数を葉とするような木

(正確には非循環有向グラフ)を組み立てているのに等しい.その様子を図 10.5に示す.

次に式の簡略化を行うが,系統的に記述するため木のパターンマッチと書換えとして定

式化した.ここでは例題の木構造に適用できるパターンとして次のもののみ用意した.

(setq *simpl-pats*

’(((mul (sub *x *1) *2) ‘(sub (mul ,*x ,*2) ,(* *1 *2)))

((mul (add *x *1) *2) ‘(add (mul ,*x ,*2) ,(* *1 *2)))

((sub (add *x *1) *2) ‘(add ,*x ,(- *1 *2)))

((add *x 0) *x)

((add (addr *x *1) (sub *y *2)) ‘(add (addr ,*x ,(- *1 *2)) ,*y))))

パターン変数のうち*xなどは任意の部分式にマッチし,*1 などは定数のみとマッチ

する.最初の 2行は,定数を加減算した後定数倍するような式は,乗算を先に行って

定数の加減算をあとから行うように変形するものである.3行目は定数を足したあと

定数を引くのを 1度の加算に,4行目は 0を加える場合には加算を止めるという簡略

Page 238: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

238 第 10章 最  適  化

化に対応している.最後の行は先に説明した,番地を取ったあと定数を加算する代り

にその定数分番地をオフセットさせるというパターンの変形である.紙面の都合から

Lispコードは略し,これらのパターンを用いて簡略化した結果を次に示す.

>(for e ’(e12 e13 e14 e15 e16 e17 e18)

(format t "~% ~A ~A ~A" e (expr-body e) (expr-depend e)))

E12 (MUL I 4) (I)

E13 (SUB E12 4) (I)

E14 (ADDR A -4) NIL

E15 (ADD E14 E12) (I)

E16 (ADD I 0) (I)

E17 (ADD E12 0) (I)

E18 (MOV I NIL) NIL

どの式を簡略化した結果がどの式かの対応関係はここには示していないが当然その

情報も計算され,元の式のうち E5,E8,E9がそれぞれ E15,E18,E12に簡略化され

ている.この情報を元に,各テンポラリに値を代入する命令を順に調べ,対応する式が

簡略化できていればその簡略化された式を計算する命令列を挿入して置き換える.つ

いでに,各式ごとに対応して 1個ずつ新しくテンポラリを用意し,式の値を計算した

らこのテンポラリにも値を複写しておく (次の広域共通式最適化に使用する).これを

行った結果を図 10.6に示す.だいぶコードが長くなったが,これは不要な中間結果の

計算が残ったままだからで,それらはこの後の不要コードの削除により取り除く.そ

の前に次の段階として共通式の削除を行う.そのためまず利用可能式の計算を行うが,

その結果は次の通り.

>(comp-availexp)

NIL

>(for b *blocks*

(format t "~% ~A ~A ~A" b (block-availin b) (block-availout b)))

B1 NIL NIL

B10 NIL NIL

B2 NIL NIL

B3 NIL NIL

B11 NIL NIL

B4 NIL (E1)

B5 (E1) (E11 E10 E18 E7 E6 E15 E14 E4 E13 E12 E2 E1)

B6 (E11 E10 E18 E7 E6 E15 E14 E4 E13 E12 E2 E1) (E10 E18 E7 E15 E14 E4

E13 E12 E2 E1)

Page 239: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.3. 一般の各種最適化 239

B1 S31: (MOV T55 T3) S44: (MOV T10 T65)

S1: (MOV 1 NOTYET) S30: (MOV T55 T53) S16: (DEREF T10 T11)

B10 S9: (ADDR A 0 T4) S45: (MOV T11 T66)

B2 S32: (MOV T4 T56) S17: (IFLE T6 T11 B7)

S2: (IFEQ NOTYET 0 B9) S33: (ADDR A -4 T58) B6

B3 S34: (ADD T58 T54 T59) S18: (MOV T6 X)

S3: (MOV 0 NOTYET) S36: (MOV T59 T5) S19: (MOV* T11 T5)

S4: (MOV 1 I) S35: (MOV T59 T57) S20: (MOV* X T10)

B11 S11: (DEREF T5 T6) S21: (MOV 1 NOTYET)

B4 S37: (MOV T6 T60) B7

S5: (SUB N 1 T1) S12: (ADD I 1 T7) S22: (ADD I 1 T12)

S26: (MOV T1 T51) S38: (MOV T7 T61) S46: (MOV T12 T61)

S6: (IFGT I T1 B8) S39: (MOV I T63) S23: (MOV T12 I)

B5 S41: (MOV T63 T8) S24: (JMP B4)

S7: (SUB I 1 T2) S40: (MOV T63 T62) B8

S27: (MOV T2 T52) S43: (MOV T54 T9) S25: (JMP B2)

S28: (MUL I 4 T54) S42: (MOV T54 T64) B9

S29: (SUB T54 4 T55) S15: (ADD T4 T9 T10)

図 10.6: 式の簡略化後のコード

B7 (E10 E18 E7 E15 E14 E4 E13 E12 E2 E1) (E18 E14 E4 E1)

B8 (E1) (E1)

B9 NIL NIL

ほとんどの式は B5と B6で計算され,B7までは伝わるが B4へ戻ると何も計算され

ていない B3からの経路と合流するため利用可能でなくなる.このプログラム例では

コード生成時に非循環有向グラフを用いたため削除できる共通式がほとんど残ってい

ないが,B7における E7(i + 1)の計算は B5での値を利用できるため削除可能である.

これを行った結果は

< S22: (ADD I 1 T12)

< S46: (MOV T12 T61)

---

> S47: (MOV T61 T12)

のように加算と MOVが削除され,代りに E7の値を保持しているテンポラリ T61から

の MOVが挿入される.

Page 240: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

240 第 10章 最  適  化

B1 S31: (MOV T55 T3) S44: (MOV T10 T65)

S1: (MOV 1 NOTYET) S30: (MOV T55 T53) S16: (DEREF T10 T11)

B10 S9: (ADDR A 0 T4) S45: (MOV T11 T66)

B2 S32: (MOV T4 T56) S17: (IFLE T6 T11 B7)

S2: (IFEQ NOTYET 0 B9) S33: (ADDR A -4 T58) B6

B3 S34: (ADD T58 T54 T59) S18: (MOV T6 X)

S3: (MOV 0 NOTYET) S36: (MOV T59 T5) S19: (MOV* T11 T59)

S4: (MOV 1 I) S35: (MOV T59 T57) S20: (MOV* T6 T10)

B11 S11: (DEREF T59 T6) S21: (MOV 1 NOTYET)

B4 S37: (MOV T6 T60) B7

S5: (SUB N 1 T1) S12: (ADD I 1 T7) S47: (MOV T7 T12)

S26: (MOV T1 T51) S38: (MOV T7 T61) S23: (MOV T7 I)

S6: (IFGT I T1 B8) S39: (MOV I T63) S24: (JMP B4)

B5 S41: (MOV I T8) B8

S7: (SUB I 1 T2) S40: (MOV I T62) S25: (JMP B2)

S27: (MOV T2 T52) S43: (MOV T54 T9) B9

S28: (MUL I 4 T54) S42: (MOV T54 T64)

S29: (SUB T54 4 T55) S15: (ADD T4 T54 T10)

図 10.7: コピー伝搬後のコード

次にコピー伝搬により大量のMOVを整理する.そのためには,まずUD-連鎖とコピー

文のデータフロー方程式を計算する.その後,各ブロックごとに,ブロックの先頭で

利用可能なコピー文の集合をもとに,1命令ずつその左辺の参照を右辺の参照に置き換

えていく.このとき同時に,ブロック内のコピー文の情報も追加していくことで,ブ

ロック内の文相互でのコピー伝搬も合せて行える.また,ブロック内で左辺か右辺が

変更されたコピー文に対応する情報は取り除いていく.これを行った結果のコードを

図 10.7に示す.最後に,不要コードの削除により,使っていないテンポラリへの代入

を取り除く.そのためには,まず生きている変数のデータフロー方程式を計算する.

>(comp-liveness ’(a n))

NIL

>(for b *blocks*

(format t "~% ~A ~A ~A" b (block-livein b) (block-liveout b)))

B1 (A N) (NOTYET N A)

B10 (NOTYET N A) (NOTYET N A)

B2 (NOTYET N A) (N A)

B3 (N A) (N A I NOTYET)

Page 241: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.3. 一般の各種最適化 241

B11 (N A I NOTYET) (N A I NOTYET)

B4 (N A I NOTYET) (A I N NOTYET)

B5 (I N A NOTYET) (T10 T11 T59 T6 T7 N A NOTYET)

B6 (T10 T11 T59 T6 T7 N A) (T7 N A NOTYET)

B7 (T7 N A NOTYET) (N A I NOTYET)

B8 (NOTYET N A) (NOTYET N A)

B9 NIL NIL

広域共通式最適化によりB6で T6と T59,B7で T7を参照するようになったため,そ

の部分だけ先に示したものと異なっていることに注意されたい (間接代入の第 2オペラ

ンドは定義ではなく参照であることに注意).これをもとに,不要コードの削除を行う

が,その Lispコードは次の通り.

(defun dead-code-elim ()

(for b *blocks* (dead-code-elim-bl b))) ; (1)

(defun dead-code-elim-bl (b &aux live d rem)

(setq live (block-liveout b)) ; (2)

(for s (reverse (block-code b)) ; (3)

(cond

((not (setq d (stat-def s))) ; (4)

(if (stat-ref s) (setq live (union (stat-ref s) live))))

((member (car d) live) ; (5)

(setq live (union (remove (car d) live) (stat-ref s))))

(t ; (6)

(push s rem))))

(setf (block-code b) (set-difference (block-code b) rem))) ; (7)

(1)ブロック単位に (2)まずブロック出口で生きている変数の集合を変数 liveに覚え

ておく.(3)次にブロックの後ろから 1文ずつ調べ,(4)変数を定義しない文について

は,その文が参照している変数も liveに加え,(5)生きている変数を書き換える文に

ついては上と同様だが (ただし書き換えている変数は liveから除き),(6)それ以外,

つまり生きていない変数を書き換えている文は削除リストに登録する.(7)最後に削除

リストにある文をまとめて消す.これを実行した結果のコードを図 10.8に示す.

例からわかるように,最適化を行うに当たっては,ある最適化を行うことで別の最

適化により改良できるパターンが生成されることが多く,その適用順序を考慮して決

める必要がある.場合によっては複数の最適化が互いに依存することもあり,そのと

きはあらかじめ決めた順序で適用して残った改良の機会はあきらめるか,より徹底す

るなら同じ最適化を複数回反復適用する.

Page 242: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

242 第 10章 最  適  化

B1 S6: (IFGT I T1 B8) B6

S1: (MOV 1 NOTYET) B5 S19: (MOV* T11 T59)

B10 S28: (MUL I 4 T54) S20: (MOV* T6 T10)

B2 S9: (ADDR A 0 T4) S21: (MOV 1 NOTYET)

S2: (IFEQ NOTYET 0 B9) S33: (ADDR A -4 T58) B7

B3 S34: (ADD T58 T54 T59) S23: (MOV T7 I)

S3: (MOV 0 NOTYET) S11: (DEREF T59 T6) S24: (JMP B4)

S4: (MOV 1 I) S12: (ADD I 1 T7) B8

B11 S15: (ADD T4 T54 T10) S25: (JMP B2)

B4 S16: (DEREF T10 T11) B9

S5: (SUB N 1 T1) S17: (IFLE T6 T11 B7)

図 10.8: 一般の最適化終了後のコード

またコード解析も各最適化において必要とされる時点を考慮して適用するが,複数

の最適化で同じ情報が必要であり,その間にコードが変形される場合にはコードの変

形に対応して抽出ずみの情報を更新するか,それが難しい場合には再度抽出をやり直

すなどの手当てが必要になる.

なお,前節で説明した最適化のうち不到達コードの削除とホイスティングについて

はここでは適用しなかったが,不到達コードの削除は式の簡略化を行った後で条件分

岐の条件が定数式かどうかを判定し,定数式であれば条件分岐を削除するか無条件分

岐に置き換えた後初期ブロックから到達できないブロックを探索して削除すればよい.

ただし条件の形によっては範囲解析を行わないと定数式かどうかわからないので,そ

のようなものまで扱うつもりであれば場合はループ最適化 (ここで範囲解析も行うこと

が多い)の後再度実施することになる.ホイスティングはどの時点で行ってもよいが,

コードの移動を伴うという点で類似しているためループ最適化と合せて行うことが考

えられる.

10.4 ループ最適化

ループの内部はコードの他の部分に比べて多数回実行されるため,ループ外コードの

実行時間を犠牲にしてでもループ内コードの実行時間を短縮することはプログラム全

体の実行時間短縮に寄与する.ループ最適化とはこのような種類の最適化全般を指す

Page 243: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.4. ループ最適化 243

ものである.なお,ループ最適化も中間コードに対して適用するのが普通であるが,読

みやすさのため以下では対応するソースコード表現を用いて説明する.

a. ループ不変文の移動  ループ不変文の移動 (loop invariant code motion)と

は,ループの周回を通じて結果が変化しない計算を行う文をプリヘッダに移動して 1

回の実行ですませるものである.まず,ループ Lについて不変な文を次のようにして

求める.

1. オペランドが定数,または到達する定義が全て L外にある変数のみから成る文

に「ループ不変」の印をつける.

2. オペランドが定数,または到達する定義が全て L外にある変数,または到達す

る定義が 1つだけであり,それが Lに含まれていて,その定義に「ループ不変」

の印がついているような変数のみから成る文にも「ループ不変」の印をつける.

上記のようにして見つかったループ不変文を全てプリヘッダに移してよいわけではな

い.たとえば,ある代入がループ不変だからといってプリヘッダに移してしまうと,そ

れはループに入る前に必ず実行される.したがって,もし本来その代入がループ内で

特定の条件が成立した場合だけ行われるはずだったとすると,プログラムの意味が変

化してしまう.この問題を避けるため,xに対する代入を行うループ不変文 sを移動

する条件として,次の制約を設ける.

(a) sは必ず 1回以上実行される.

(b) xと同じ変数を定義する文は L内では sだけである.

(c) L内の全ての xの使用について,そこに到達する定義は sだけである.

(a)を調べるにはループの出口となるブロック (Lに含まれない後続ブロックをもつよ

うな Lのブロック)全てが sを含むブロックに支配されるかどうかを調べる.もしそう

なら,sを通らずにループを出る方法はないのだから,sは必ず 1回以上実行される.

これらの制約を満たすことは sを移動できる必要十分条件ではないが,手持ちのコー

ド解析情報から調べるのが容易であり,移動をそのような sに限ったとしてもある程

度の最適化が行えることが経験的に知られている.

whileループや forループではループ本体が 1回も実行されない可能性があるので,

条件 (a)がある限りループ本体に含まれる文はどれも移動できない.そこで,これを

次のように変更することも考えられる.

Page 244: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

244 第 10章 最  適  化

(a’) sは必ず 1回以上実行されるか,または xは L外では参照されない.

ただし,この場合本来は実行されない計算が実行されることに注意が必要である.例

えば移動する文に除算が含まれていると,本来のコードでは起きない 0割りエラーが

最適化後のプログラムに発生することがある.次の例を見てみよう.

i := 0;

while(i < 10 && x > 0) {

a[i] := y / x; i := i + 1; }

ここで xが 0だとループそのものが実行されないから 0割りも起きない.ところが

y/xがループを不変式として移動し,

i := 0;

t1 := y / x;

while(i < 10 && x > 0) {

a[i] := t1; i := i + 1; }

のようにすると,これまで起きなかった 0割りエラーが発生し得る.この種の問題で

は 0割りが一番典型的なので,除算だけは外に出さず,加減乗算のオーバフロー/アン

ダフローは無視して不変式を外へ出すという方針もあり得る.より厳格にこれに対処

するには,ループが 1 回以上実行されるとわかっているときだけこれらの文も実行す

る.つまり

i := 0;

if(i < 10 && x > 0) {

t1 := y / x;

while(i < 10 && x > 0) {

a[i] := t1; i := i + 1; } }

とする.これはかなり冗長に見えるが,定数式の畳み込みや不要コードの削除によって

i := 0;

if(x > 0) {

t1 := y / x;

while(i < 10) {

a[i] := t1; i := i + 1; } }

Page 245: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.4. ループ最適化 245

になるはずで,これだと人間が手で不変式の計算を移動したのと変りない.

図 10.8のコード列では移動できるループ不変文は外側ループについてのS5,S9,S33

であり,これらをプリヘッダ B11に移すことができる (ただし上述の「厳格な立場」に

立つ場合には変数 notyetや変数 iの値を追跡して内側ブロックが 1回以上実行され

る条件のとき — つまり nが 1より大きいとき — のみこれらを実行するようなコード

を作成するべきである).

b. ループ内分岐の移動  ループ不変文の検出に伴い,if文などの条件式がルー

プ不変であることがわかる場合がある.つまり

t1 := ...

while(...) {

C;

if(t1)

A;

else

B;

D;

}

のようなものである.この場合,ループの中で繰り返し同じ分岐判定をするのは無駄

だから,これを

t1 := ...

if(t1)

while(...) {

C; A; D; }

else

while(...) {

C; B; D; }

のように変形することで,明らかにコード量は増えるが実行時間は短くできる.

c. 帰納変数の最適化  ループ最適化で重要なものの 1つに,ループとともに

変化する変数 (特にループ周回ごとに定数値だけ増減するもの)に関するものがある.

まず次の用語を定義する.

定義 永続ループ変数 (persistent loop variable)とは,ループ入口において生きていて,

なおかつループ内で定義される (言い換えればループ内でまず参照され,続いて

定義される)変数をいう.

Page 246: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

246 第 10章 最  適  化

定義 ループ帰納変数 (loop induction variable)とは,ループの周回とともに値が一定

値ずつ増加 (または減少)するような変数をいう.

多くの場合ループを制御する変数は永続ループ変数になる.次の例を考えてみる.

i := 0; p := top;

while i < x do begin while p <> nil do begin

... ...

i := i + 1 p := p^.next

end end

左の場合 iは永続ループ変数でありかつ帰納変数でもある.右の場合 pは永続ルー

プ変数だが帰納変数ではない.

帰納変数は永続ループ変数である場合 (基本機能変数とよばれる)以外に別の帰納変

数の値をもとに値を計算している場合があるが,そのような帰納変数ば基本機能変数

に変換できる (ループ 1周回ごとに定数を加減するコードに置き換える).このような

変換は多くの場合乗算を加減算で置き換えることになるので,演算強さの軽減が行え

ることになる.

さらに,別の帰納変数と常に同じ値をもつことがわかる帰納変数や,条件判定にし

か現れず,その判定条件を別の帰納変数を使うように書き換えられる帰納変数は取り

除いてしまうことができる.また,帰納変数はループ入口での値と終了条件を調べる

ことで取り得る値の範囲を比較的容易に知ることができ,この情報を用いることで条

件式が定数式である場合を多く検出できる.まず基本帰納変数の検出は次のようにし

て行える.

• 変数 iが永続ループ変数であり,かつループ内での iに対する定義 sが i := i

± C(ただし C はループ不変)の形であり,sがループ周回ごとに必ず 1回実行

されるならば iは帰納変数である.

sがループ周回ごとに必ず 1回実行されるかどうかはループ帰辺の直前のブロック bが

sを含むブロックに支配されるかどうかで調べられる.次に,基本帰納変数以外の帰納

変数を考慮する.

• 変数 j が永続ループ変数でなく,かつループ内の j に対する定義 sが j := i *

c ± d(ただし iは帰納変数,c,dはループ不変)の形であり,sがループ周回

ごとに必ず 1回実行されるなら j は帰納変数である.

Page 247: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.4. ループ最適化 247

ここで iが基本帰納変数であるとき,j は iの家族 (family) であるという.iが基本帰

納変数でないとき,iは別の基本帰納変数 i0の家族なので,

1. iへの定義と sの間に i0への定義がはさまっていない.

2. ループ外の iへの定義が sに到達することがない.

の 2条件が成り立てば jも i0をもとに計算するように (つまり i0 の家族に)できる.例

えば次のコードを考える.

i0 = 0; i0 = 0;

while(...) { while(...) {

i = i0 * 2 + 1; j = i * 2 + 3; /* s */

... i = i0 * 2 + 1;

i0 = i0 + 1; ...

j = i * 2 + 3; /* s */ i0 = i0 + 1;

... }

}

ここで左側のコードは条件 1,右側のコードは条件 2を満たさない.右の場合は j

の値がループの最初の周回と 2周回目の間で定数増減にならないので帰納変数ではな

い.左の場合は jの値が sの時点におけるより 1周回前の i0の値によって計算しなけ

ればならないことを注意しておけば帰納変数として最適化することはできる.次に永

続ループ変数でない帰納変数 j を永続ループ変数に変換するが,その手順は次の通り.

1. 新しい変数 uを導入し,その初期値設定をループのプリヘッダに追加する.

2. 家族の基となっている変数の更新の直後に,uの増減を行う定義を追加する.

3. jの定義を j := uに取り替える.

もちろん,新しく追加される複数の変数が同じ初期値,増減値,挿入位置であるなら

これらを 1つで兼ねる.上の例に対して適用した結果は次のようなコードとなろう.

i0 = 0; u = 1; v = 5;

while(...) {

i = u;

...

i0 = i0 + 1; u = u + 2; v = v + 4; j = v;

...

}

Page 248: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

248 第 10章 最  適  化

B1 S3: (MOV 0 NOTYET) S19: (MOV* T11 T59)

S1: (MOV 1 NOTYET) B11 S20: (MOV* T6 T10)

B10 S49: (ADD T58 4 T67) S21: (MOV 1 NOTYET)

S33: (ADDR A -4 T58) S50: (ADD T4 4 T68) B7

S9: (ADDR A 0 T4) B4 S52: (ADD T67 4 T67)

S5: (SUB N 1 T1) S58: (IFGT T67 T70 B8) S53: (ADD T68 4 T68)

S56: (MUL T1 4 T69) B5 S24: (JMP B4)

S57: (ADD T58 T69 T70) S11: (DEREF T67 T6) B8

B2 S16: (DEREF T68 T11) S25: (JMP B2)

S2: (IFEQ NOTYET 0 B9) S17: (IFLE T6 T11 B7) B9

B3 B6

図 10.9: ループ最適化後の命令列

この状態では演算強さは軽減された代りに値コピーが増えているが,この後コピー

伝搬を行うことで不要な複写は削除できる.加えて,帰納変数を基本機能変数に変換し

た結果,これまで他の帰納変数を計算するためだけに使われていた帰納変数の参照が

なくなる場合がある.その場合はその変数の計算は不要コードとして削除できる.ま

た,同じ参照でも条件式においてループ不変式と比較されるもののみが残った場合は,

これを別の帰納変数に関する判定に置き換えて取り除くことができる.前節の 4つ組

コードにおいて,これらの最適化を行った結果のコードを図 10.9に示しておく 4.

d. ループ展開  ループ展開 (loop unrolling)とは,ループ本体を複数回重複さ

せることによりループのための分岐や判定の実行回数を減らす最適化をいう.最も極

端には,ループの周回回数が翻訳時にわかり,その値があまり大きくなく,ループ本

体も小さい場合にはその回数だけ本体を展開することでループを全くなくしてしまう

場合もある.

そのように極端でなくても,ループ周回数が偶数であるとわかればループ本体を 2

回展開することにより,展開後のループ周回数は半分になり,ループ制御に要するオー

バヘッドは半分にできる.また,展開された 2 個のループ本体について共通式のくく

り出しができる場合も多く,そのときはさらに実行速度を高められる.ループ周回数

がわからない場合にはループの終了判定を展開した本体 1個につき 1回ずつ行う (例え

4さらに頑張るなら,T67 と T68 が常に 4 違うことを解析すれば加算する前の値をとっておいて利用することで加算を 1回節約するようにできるが,多くの計算機では転送と加算の所要時間が変らないので,この場合はそこまでしても引き合わない.

Page 249: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.4. ループ最適化 249

ば下記の左のコードを右のように変形する).

while(C) { while(C) {

B; B;

} if(!C) break;

B;

}

この場合でもループ先頭への分岐は減ることになる.また,この変形の結果,共通

式のくくり出しが可能になる場合も多い.

e. ループ融合  ループ融合 (loop fusion)とは,同一周回数をもつループが隣

接している場合にそれらをまとめて 1つのループにしてしまうことをいう.例えば次

のような例である.

for i := 1 to n do a[i] := i; for i := 1 to n do begin

for i := 1 to n do b[i] := n - i; a[i] := i; b[i] := n - 1

end;

融合によってループ制御のオーバヘッドは半分になる.制御変数の範囲が同一でな

い場合には単純に半分ではないが,終了判定とループ先頭への分岐は節約できる.も

ちろん,融合しても元のコードと意味が変らないためには双方のループ本体に干渉が

ないことが必要である.

f. 配列の線形化  ループ融合と類似しているが,2次元配列 (一般には多次元

配列)を順番にアクセスするコードはループの入れ子になる.

a: array[1..10][1..10] of ...;

...

for i := 1 to 10 do

for j := 1 to 10 do a[i][j] := 0.0;

この場合,配列 aは主記憶上に連続して配置されているので,それを仮想的に 1次

元配列だとみなして 1重ループに変換できる場合がある.

for i := 1 to 100 do a[i] := 0.0;

これを配列の線形化 (linearizaton)とよぶ.線形化はループのオーバヘッドを減らす

効果があり,またベクトルプロセサの場合にはベクトル長を長くするという点でも実

行効率を高める効果がある.

Page 250: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

250 第 10章 最  適  化

main

葉の手続き

図 10.10: 呼出しグラフ

線形化が行えるためには,最内側のループにおいて配列の一番最後の次元の添字式

が対応する次元の上限から下限まで変化することと,その 1 つ外側のループで 1つ前

の次元の添字式が 1つずつ変化することが条件となる.ここで最後 (最左側)の次元と

なっているのは配列が行単位 (column-wise)で配置される言語の場合であり,Fortran

のように列単位 (row-wise)で配置される場合には最初 (最右側)から順に調べる.

10.5 手続き間最適化

1つのプログラムを多数の手続きに分割して構成することは今日の一般的なプログラミ

ングスタイルであり,このため手続きの呼びや戻りの処理,および引数受渡しに伴う

オーバヘッドはプログラム実行時間のうち無視できない割合を占める.そこで,個々の

手続きの内部の最適化に加え,手続きの呼出しをまたがった最適化 (手続き間最適化)

を通じてこれらのオーバヘッドを軽減することも,今後のコンパイラの重要な課題で

ある.

多数の手続きから成るプログラム全体を対象として一括して詳細な解析を行うこと

は実用的でないため,手続き間最適化において用いられる手法は手続き内の最適化と

はかなり毛色が異なっている.

手続き間最適化においても,他の最適化と同様,頻繁に実行される部分を優先的に

改良するのが得策である.これは手続き内最適化ではループ内部を最適化することに

相当したが,手続き間最適化の場合は呼出しグラフ (call graph,手続き間の呼出し関

Page 251: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.5. 手続き間最適化 251

係を図 10.10のように描いたもの)の葉に近い手続き呼出しから最適化することに相当

する.その理由は,手続きAが Bを呼ぶ場合,Aの 1回の実行中に Bを 1回以下しか

呼ばないということはあまりなく,繰り返し Bを呼ぶことの方が多いためである.以

下では手続き間最適化の代表的な手法について解説する.

a. その場展開  手続きのその場展開 (inline expansion)とは,手続き呼出しの

場所に呼ばれる手続き本体のコードを埋め込んでしまうことである.ただし,その実現

にはいく通りかの選択肢があり,特に引数の受け渡しに関して注意を払う必要がある.

まず考えられるのは,ソースコードレベルで手続き呼出しの箇所に手続き本体のコー

ドを埋め込んだ後通常通りに翻訳することである.この方法はコンパイラ本体に手を

加えなくても実装可能であり,コンパイラによる最適化の機会が失われないという利

点をもつ.一方,弱点としては次の点が考えられる.

(a) 呼ぶ側と呼ばれる側双方のソースコードが同時にアクセスできなければならな

い.このため分割翻訳には適さない.

(b) ソースコードの量が展開により増加するため,処理量が増えて翻訳時間が増大す

る.目的コードも当然大きくなる.

(c) 展開する側とされる側での名前の衝突に配慮する必要がある.

(d) 引数を字面上で置き換えるという一番容易な方法を採用すると必然的に受け渡し

機構が名前呼びになってしまい,通常の呼出し時の受け渡し機構と (少なくとも

多くの言語で)一致しなくなる.

最後の点については,C言語では最初からプリプロセッサによるマクロ機能の一貫と

してその場展開を規定し,機構の違いにはプログラマが配慮するとの立場をとる.あ

るいは,手間は増えても図 10.11のように中間変数を適宜導入して値呼びや参照呼び

が実現されるように手当てしながら展開することも可能である.

次のレベルとして,抽象構文木や中間コードの段階で展開することが考えられる.当

然,コンパイラに手を加える必要はあるが,最適化の可能性は失われない.そしてソー

スコードレベルでの展開にあった問題点のうち (b),(c)は基本的に回避される.(d)に

ついては木や中間コードのレベルでは値呼びや参照呼びが実現されるように手当てし

ながら展開することは容易である.(a)についても,中間コードファイルを書き出すよ

うなコンパイラ構成であれば,展開フェーズを別に設けてその前までは通常通り分割

翻訳を行うようにできる.

Page 252: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

252 第 10章 最  適  化

proc main; double_print(read_num());end;

proc double_print(n:integer); print_num(n + n);end;

その場展開

proc main; print_num(read_num() + read_num());end;

proc main; arg1 := read_num(); print_num(arg1 + arg1);end;

(A) (B)

図 10.11: その場展開における引数の処置

最後に,目的コードのレベルで展開を行うことも考えられなくはないが,この場合

期待できるのは呼び/戻り命令の節約程度であり,呼出しをまたがった最適化の可能性

という点では手間の割に得策ではない.

いずれの実現にしても,その場展開は前項で述べた原理に従って,葉の手続きから

順次展開を進めて行き,コードサイズと得られる実行時間の節約見込みのトレードオ

フを見つくろってどこで止めるかを決定することになろう (全ての呼出しを展開するこ

とはコードサイズの点から実用的ではないし,再帰呼出しがあった場合には全て展開

することは原理的にできない).

b. 入口と出口の簡略化  その場展開を行わず呼出しを実施するとしても,呼

出しに伴うオーバヘッドの多くは呼び/戻りの命令自体よりは入口と出口の処理に起因

しており,その処理を最適化する可能性は多く残されている.

まず,葉の手続き (他の手続きを呼び出すことのない手続き)については,動的チェ

インやスタックフレームを毎回構築しては元に戻すのは無駄である.そこで,入口と

出口におけるこれらの操作を省略し,その手続き内については戻り番地格納位置を指

したままのスタックポインタを起点としてフレーム参照を行うようにできる (小さい手

続きであれば,フレーム参照は全く不要かもしれない).葉の手続きでなくとも,他の

手続きを呼び出す場所が限定されていて頻繁に実行されないと判断できれば (例えば

エラー処理の箇所などがそういう条件にあてはまる),図 10.12のようにその箇所の前

後のみでフレームの構築と復元を行うようにでき,頻繁に実行される経路については

オーバヘッドが削減できる.

以上は実は各手続き単独で行うことができるが,手続き間で情報を流通することが

できれば,「自分が呼び出している手続き群のどれもがフレームポインタを参照しない」

Page 253: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

10.5. 手続き間最適化 253

頻繁に実行;呼出しを含まない

たまに実行;呼出しを含む

フレーム構築

復元

入口点

戻り

図 10.12: 退避回復範囲の縮小

とわかったときには中間の手続きにおいても同様の省略を行うことができる.

c. 手続き間レジスタ割付け   8章で述べたように,手続きの呼出し時は呼び側

保存のレジスタ群をスタックに退避し,戻ってきたときに回復する.一方呼ばれ側保存

のレジスタ群は呼ばれた手続きの入口で退避し,戻りの直前に回復する.しかし,もし

呼び側と呼ばれ側で使用するレジスタが排他的であれば,これらの退避回復を一切しな

いですむ.これを可能な範囲で実現するのが手続き間レジスタ割付け (interprocedural

register allocation)である.

これを実現するにはやはり前述の原理に従い,葉の手続きから先に割付けを開始し,

呼出し関係の木を遡りながら新しい手続きが出てくるつど,「それが呼出す手続き群で

使用しているレジスタをよけて」使用するレジスタを割り付ける.どこかから先はレ

ジスタが足りなくなって通常の退避回復に戻ることになるが,葉に近い手続きほど頻

繁に呼ばれるという前提にたてばこれで十分役に立つ.複数の手続き間で呼出し関係

が環になっている場合 (つまり再帰呼出し)には,その環の 1周を通じて一群の手続き

が使うレジスタ群一式が退避されるようにする.

また,引数や返値の受け渡しについても,手続き間レジスタ割付けによればできる

だけレジスタで受け渡すようにできる.この場合,単なるレジスタ渡し規約とは異な

り,引数を受け取るレジスタと呼び先に引数を渡すレジスタを違えておけるため余分

な転送を節約することができ,また引数の個数が多くても利用できる限りのレジスタ

を活用できる.

Page 254: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

254 第 10章 最  適  化

10.6 練 習 問 題

10-1. 自分の手持ちのプログラムの中からできるだけ「流れがごちゃごちゃしている」

と思うものを 1つ取り上げ,そのフローグラフを描いてみよ.そのフローグラフ

に含まれているループを全て列挙せよ.それらは全て自然なループか,それとも

自然でないループも含まれているか.

10-2. 10.2.3項で取り上げたデータフロー問題の中から好きなものを 1つ選び,問題

10-1で取り上げたプログラムについて解いてみよ.ただし,解く前にまず解を

「直観的に」予想してみること.直観的に考えた予想と方程式の解はどれくらい

一致したか.一致しなかった部分はどうして一致しなかったのか.

10-3. 自分の手元にある最適化を行うコンパイラを 1つ選び,本章であげた最適化の

うちどれとどれが組み込まれているか調べてみよ.基本的には,それぞれの最適

化が有効であるようなソースコードを食べさせて出力を調べることを繰り返せば

よいはずである.

10-4. 自分の手持ちのプログラムでループ周回数が多いため時間がかかるものを 1つ

選び,10.4節であげたような各種のループ最適化をソースコードレベルで施して

それぞれの効果を調べよ (コンパイラの最適化機能は止めておくこと).

10-5. 自分の手持ちのプログラムで手続き呼出しが多いため時間がかかるものを 1つ

選び,手で手続き間最適化を施してその効果を調べよ.例えばその場展開はソー

スコードレベルで行える.もしやる気があれば,アセンブリコードになった状態

で入口/出口処理を細工したり手続き間レジスタ割付けを行ってみるのも面白い.

Page 255: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

255

第11章 目的コード生成

コンパイラの旅もようやく終りに近づき,最終製品である目的コードを生成する段と

なった.ひとくちに目的コード生成といっても,目指すコードの品質や,多様な目的

機械への移植性の程度により,その難しさの程度は様々である.本章では,目的コー

ドという機械依存の対象をどのように一般化してうまく扱うかを中心に述べ,また特

にコンパイラにとって重要な目的機械の特性への対処についても解説する.

11.1 組単位の展開によるコード生成

コード生成の過程を構造する方法の 1つは,中間コード命令の種類ごとにコード生成

手続きを用意し,それを呼び出すことでコード生成を行わせることである.これを組

単位の展開によるコード生成とよぶ.中間コード命令が 20種類あるとして,「その 20

種類ぶんの生成手続きを機械 A用に書けば,機械 Aの目的コードを出すコンパイラが

できます」というのはいかにもわかりやすい.また,目的コードが正しくない場合に

どの手続きがおかしいかすぐわかるという利点もある.このため実行時の効率よりは

コンパイラの単純さや移植の容易さを重視する場合には組単位のコード生成は有力な

選択肢である.

組単位という枠組の中でも,各命令の性質 (オペランドが交換可能かどうか等),オ

ペランドの種類 (変数か定数か,定数ならその値は何ビットで表せるか等),利用可能

な資源 (使えるレジスタはどれか,すでにレジスタに入っている値はどれか等)により

最も効率的な命令列は異なる.これらの場合分けを行い,どの命令列を生成するかを

決めることをケース分析 (case analysis)とよぶ.ケース分析は,その程度によっては

コード生成中で最も手間のかかる部分になるので,どの程度まで細密なケース分析を

行うかは十分検討して決める必要がある.

Page 256: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

256 第 11章 目的コード生成

例として,図 10.8の中間コード (のリスト形式)から最低限のケース分析を伴う組単

位のコード生成を行わせてみる.目的コードとしては Sun-3 (CPUはMC68020)用の

UNIXアセンブラを対象とする.コード生成に先立って,各変数ごとにその変数のス

タック上の位置 (具体的にはフレームポインタからの変位)を割り当てておく必要があ

る.ここではその値を各変数の off属性に格納しておくものとする.また,この値が 0

のものは広域変数だということにする (C言語の mainから呼べるようにしたため,配

列の値渡しができないので配列 aを広域変数とする).これらの情報は本来は記号表か

ら計算するわけだが,ここでは次のように直接セットしておく.

(defmacro var-off (v) ‘(get ,v ’off)) ; アクセス用マクロ(setf (var-off ’A) 0) ; aは広域(setf (var-off ’N) +8) ; nは第 1引数(setf (var-off ’I) -4) ; ここから局所変数(setf (var-off ’NOTYET) -8)

(setf (var-off ’T1) -12) ; テンポラリも同様(setf (var-off ’T4) -16)

… ; 途中略(setf (var-off ’T59) -44) ; これが最後

次に,コード生成はいきなりアセンブリコードを出力することにして,次のような

コード生成ドライバを用いる.

(defun ocg (code)

(for s code ; (1)

(if (atom s)

(format t "~%~A:" s) ; (2)

(apply (symbol-function (car s)) (cdr s))))) ; (3)

(1)各中間命令ごとに個別に生成を行うが,(2)ラベルであれば同名のアセンブリコー

ドラベルを生成し,(3)それ以外なら 9章のときと同様,中間命令と同名の関数を命令

の各オペランドを引数として呼び出す.各中間命令ごとの生成ルーチンであるが,ま

ず加算から示す.

(defun add (src1 src2 dst)

(cond ((numberp src1)

(format t "~% movl #~D,d0" src1)

(format t "~% addl a6@(~D),d0" (var-off src2)))

((numberp src2)

Page 257: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.1. 組単位の展開によるコード生成 257

(format t "~% movl a6@(~D),d0" (var-off src1))

(format t "~% addl #~D,d0" src2))

(t

(format t "~% movl a6@(~D),d0" (var-off src1))

(format t "~% addl a6@(~D),d0" (var-off src2))))

(format t "~% movl d0,a6@(~D)" (var-off dst)))

オペランドが定数であるかどうかでアセンブリコードを変える必要がある.定数でな

い場合は全てフレームポインタ (a6)±オフセットのアドレシングモードを使えばすむ

(広域変数は必ずいったん番地をとるような中間コード設計にしたことに注意).また,

定数の畳み込みが終っていれば両方が定数ということはないので,結局 3通りの場合

分けですむ.MC68020では計算はレジスタ上で行わなければならないので d0レジス

タを使うことにして,いずれの場合でもまず第 1オペランドを movl(32ビット転送)命

令で d0にロードし,第 2オペランドを addl(32ビット整数加算)命令で足し込む.最

後に行き先の変数に結果を転送する.四則演算の残りも同様である.

次に番地ロード命令を示す.MC68020では番地はアドレスレジスタに入れる必要が

あるので,このため a0を使う.

(defun addr (src off dst &aux ident)

(setq ident (string-downcase (symbol-name src)))

(if (zerop (var-off src))

(format t "~% lea _~A+~D,a0" ident off)

(format t "~% lea a6@(~D),a0" (+ (var-off src) off)))

(format t "~% movl a0,a6@(~D)" (var-off dst)))

変数が広域変数かスタック上の変数かによってオペランドの形は違うが,いずれも

lea(実効番地ロード命令)で番地をとることができる.あとは行き先に格納するだけで

ある.

間接参照と間接代入は転送の向きが違うだけで,まずどちらも a0 に番地をもってき

て,このレジスタに対する間接番地指定で転送する.

(defun deref (src dst)

(format t "~% movl a6@(~D),a0" (var-off src))

(format t "~% movl a0@,a6@(~D)" (var-off dst))))

(defun mov* (src dst)

(format t "~% movl a6@(~D),a0" (var-off dst))

(format t "~% movl a6@(~D),a0@" (var-off src))))

Page 258: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

258 第 11章 目的コード生成

ifxxは cmp命令を出力するところは四則演算と同様で,その後すぐ対応する条件分

岐を実行すればよい.

(defun ifgt (src1 src2 dst)

(cond ((numberp src1)

(format t "~% movl #~D,d0" src1)

(format t "~% cmpl a6@(~D),d0" (var-off src2)))

((numberp src2)

(format t "~% movl a6@(~D),d0" (var-off src1))

(format t "~% cmpl #~D,d0" src2))

(t

(format t "~% movl a6@(~D),d0" (var-off src1))

(format t "~% cmpl a6@(~D),d0" (var-off src2))))

(format t "~% bgt ~A" dst))

他の条件の場合も同様である.最後に無条件分岐であるが,単に jmp命令になるだ

けである.

(defun jmp (dst) (format t "~% jmp ~A" dst))

以上の関数群により図 10.8の中間コード命令列から目的コードを生成したものを図

11.1に示す.ただし,広域ラベルの宣言と手続きの入口 (動的チェインを延ばし,ス

タック上の領域を割り当てる)/出口 (環境ポインタを復元し,戻る)の各 2命令は手で

挿入した.これをアセンブラで翻訳したものは例えば次のようなC言語による主プロ

グラムと結合して動かせる.

int a[大きさ];

main() {

配列 aにデータを用意するbubble(n);

...

}

図 11.1を見ると,毎回主記憶から値をロードして演算後書き戻すため,命令列が長

く,ある番地に格納した値をすぐまたロードするなど無駄な命令列が多く見られる.組

単位のコード生成では本質的に,ある命令と次の命令の間での情報の流通がないため,

それに起因して無駄な命令列が生成される.また,複数の中間コード命令の動作が 1

つの機械命令で実現できる場合にもそれを活用できない.

Page 259: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.1. 組単位の展開によるコード生成 259

.text cmpl a6@(-12),d0 movl a6@(-28),a0

.globl _bubble bgt B8 movl a0@,a6@(-32)

_bubble: B5: movl a6@(-20),d0

link a6,#0 movl a6@(-4),d0 cmpl a6@(-32),d0

addl #-48,sp mulsl #4,d0 ble B7

B1: movl d0,a6@(-36) B6:

movl #1,a6@(-8) lea _a+0,a0 movl a6@(-44),a0

B10: movl a0,a6@(-16) movl a6@(-32),a0@

B2: lea _a+-4,a0 movl a6@(-28),a0

movl a6@(-8),d0 movl a0,a6@(-40) movl a6@(-20),a0@

cmpl #0,d0 movl a6@(-40),d0 movl #1,a6@(-8)

beq B9 addl a6@(-36),d0 B7:

B3: movl d0,a6@(-44) movl a6@(-24),a6@(-4)

movl #0,a6@(-8) movl a6@(-44),a0 jmp B4

movl #1,a6@(-4) movl a0@,a6@(-20) B8:

B11: movl a6@(-4),d0 jmp B2

B4: addl #1,d0 B9:

movl a6@(8),d0 movl d0,a6@(-24) unlk a6

subl #1,d0 movl a6@(-16),d0 rts

movl d0,a6@(-12) addl a6@(-36),d0

movl a6@(-4),d0 movl d0,a6@(-28)

図 11.1: 組単位のコード生成による目的コード

Page 260: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

260 第 11章 目的コード生成

11.2 解釈実行型コード生成

組単位コード生成の弱点を補う1つの方法が,解釈実行型コード生成 (interpretive code

generation)である.この方法では,中間コード命令を読んだとき対応するコードを全

て生成する代りに,中間コード命令によって指示される動作をコード生成器内部で (記

号的に)保持する.つまり記号実行を行うインタプリタのような動作を行う (そのため

解釈実行型とよばれる).そして,適当なタイミングごとに,インタプリタ内部の状態

と同じ状態が目的機械上で実現されるようなコードを生成する.「適当なタイミング」

とは具体的には次の場合である.

• インタプリタ内部で保持しておかなくてもコードを出してかまわない場合.

• インタプリタ内部の枠組より複雑な状態が必要になる場合.

• 基本ブロックの終りまで来たとき.

最後の条件については簡単で,どの実行経路を通っても目的機械の状態がソースコー

ドと整合しているためには,各ブロック末尾では必ず「そのときあるべき状態」になっ

ているようにコードを出さなければならない.一方,前 2者はややわかりにくいが,イ

ンタプリタ内部で目的コードの 1 命令 (ないしそれ以上)に相当する状態を蓄えること

で,複数の中間コード命令を 1つの目的コード命令に対応させるようなコード生成を

可能にすることが要点となっている.例えばインタプリタの状態として「どのレジス

タは現在どの変数と同じである」および「どの変数の値はまだ計算していないが,こ

れこれの式に等しい」という情報を任意個もてるものとして,これを

(レジスタ 変数 式)

の 3つ組で表す (ただし「レジスタ」か「式」のどちらかは必ず nilとなる)枠組を考

え,これを先の例のブロック B5に適用してみると例えば次のようになろう.

入力中間コード 状態の変化 出力コード-------------------------------------------------------------

(MUL I 4 T54) 1:(d0 i nil) mov a6@(-4),d0

2:(nil t54 (* d0 4))

(ADDR A 0 T4) 3:(a0 t4 nil) lea _a,a0

(ADDR A -4 T58) 4:(a1 t58 nil) lea _a+-4,a1

(ADD T58 T54 T59) 5:(nil t59 (+ a1 (* d0 4)))

Page 261: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.2. 解釈実行型コード生成 261

(DEREF T59 T6) 6:(d1 t6 nil) mov a1@(d0:l:4),d1

(ADD I 1 T7) 7:(nil t7 (+ d0 1))

(ADD T4 T54 T10) 8:(nil t10 (+ a0 (* d0 4)))

(DEREF T10 T11) 9:(d2 t11 nil) mov a0@(d0:l:4),d2

(IFLE T6 T11 B7) ***

ここで配列アクセスにインデックスアドレシングモードが出せるのは内部状態とし

て「番地+スケール×レジスタ」という式を保持しておくようにしたからであること

に注意されたい.最後に***のところでブロックの終りになるが,ブロックの出口で生

きている変数は計算されていなければ計算し,レジスタに載ったままであれば主記憶

上の正しい場所に書き戻さなければならない.その後でブロック出口のコード (比較と

分岐)を出力する.

10:(a1 t10 nil) lea a0@(d0:l:4),a2

remove 8

mov a2,a6@(-28)

mov d2,a6@(-32)

11:(a2 t59 nil) lea a1@(d0:l:4),a3

remove 5

mov a3,a6@(-44)

mov d1,a6@(-20)

add #1,d0

mov d0,a6@(-24)

cmpl d2,d1

ble B7

この場合は入力コードが広域最適化後のもので,ブロックをまたがって生きている

テンポラリが多いため書き戻し量も多いが,局所最適化しか行わないコンパイラでは

ブロックをまたがって生きているのは通常の変数のみであり書き戻すべき値もそれら

の変数に限られる.また,ここではコード生成の途中でレジスタが不足することはな

かったが,現実にはブロックが長くなるとレジスタにある値を主記憶に書き戻してレ

ジスタを空ける必要が生じる.このとき内部状態に含まれる式のうちあけようとして

いるレジスタに依存しているものは値を計算してしまう必要があるが,そのためのレ

ジスタまで足りないとそれ以上進めなくなるので,そのような事態に陥らないように

絶えず配慮している必要がある.

ここでは中間コードが 4つ組だったためインタプリタの状態はテンポラリとレジス

タまたはそのテンポラリに保持される式の組で表されたが,後置コード型の中間コー

Page 262: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

262 第 11章 目的コード生成

ドに対して適用する場合にはインタプリタ状態は仮想的なスタックとその各位置の値

を保持しているレジスタ名またはその位置に保持されている式の情報となろう.

11.3 レジスタ割当てとレジスタ割付け

現在実用に供されている多くの CPUでは,演算を行うのにレジスタを使うため,個々

の演算においてどのレジスタを使うかを決めながらコード生成を進める必要がある.こ

の作業をレジスタ割当て (register assignment)とよぶ.さらに,多数のレジスタをも

つ CPUでは一部 (ないし全部)の変数を主記憶の代りにレジスタに割り付けることに

より,時間のかかる主記憶アクセスを減らしコードを高速化できる.この作業をレジ

スタ割付け (register allocation),特に基本ブロックをまたがって行う場合には広域レ

ジスタ割付け (global register allocation)とよぶ.

11.1節で説明したコード生成方式では,常に特定のレジスタだけを式の評価に用い,

あとのレジスタは使用しなかった.11.2節で示した例では空いているレジスタを順に

使用し (レジスタ割当て),レジスタが不足しない限りは変数の値はブロック境界を越

えない範囲でそのままレジスタに置いておき機会があれば再利用した (ブロック内レジ

スタ割付け).しかし先頭から順にコード生成を行いながらそのつどレジスタを割り当

てる方法には限界がある.そこで,広域最適化を行うコンパイラでは,通常コード生

成に先立ち変数の生死の情報に基づいて広域レジスタ割付けを行う.その意味ではレ

ジスタ割付けは最適化の 1種と言える.ただしレジスタ割付けの手法そのものは目的

機械に依存しないが,使用可能なレジスタ数やレジスタの種類に関する制約は目的機

械に依存するので,必ずしも他の機械独立な最適化と同列には扱えない面もある.

ここでは,レジスタ割付けを統一的に扱う枠組として,レジスタ彩色 (register col-

oring)に基づく方法を説明する.コード上の各変数には,図 11.2左に示すように生き

ている範囲 (実線で表す)が存在する.ここで,例えば変数 aと cについてみると,そ

の生きている期間に重なりがないので,まずレジスタ d1を cのために使用し,その

後 aのために使用しても問題ない.一方 aと eは生存期間に重なりがあるためそのよ

うなことは行えない.実際のコードには分岐や合流があって一直線ではないが,同様

に生存期間の重なりを考えることができる.この情報を,各変数を頂点とし,生存期

間に重なりがある頂点間を辺で結んだグラフで表現する.これをレジスタ干渉グラフ

Page 263: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.3. レジスタ割当てとレジスタ割付け 263

a b c d e

a

b

c

e

d

図 11.2: レジスタ干渉グラフ

(register interference graph)とよぶ.なお,グラフ理論では,各頂点について,そこ

から出ている辺の数を頂点の度数 (degree)と言い,互いに辺で結ばれている頂点どう

しを隣接している (adjacent)と言う.そして,k個のレジスタを一群の変数に割り当

てるという問題は,グラフの隣接する頂点が同じ色にならないという条件のもとでグ

ラフの頂点に k色を割り当てるという問題に定式化できる.図 11.2の場合は,右側に

示したようにグラフを 4色で塗ることができるので,4個のレジスタがあればこれら

の変数をレジスタに割り当てられる 1, 2.

では Lispにより彩色を実現してみる.まず干渉グラフを求めるが,これは前章での

死んでいるコードの削除と同様,ブロック出口から逆向きに生きている変数を追跡し,

同時に生きている変数の対を集めればよい.ここでは aと bが同時に生きている場合

には変数*edges*に (a . b)と (b . a)をともに加える.

(setq *edges* nil) ; 辺の集合(defun push-inf-pair (x y) ; 辺を追加する

(pushnew (list x y) *edges* :test #’equal)

(pushnew (list y x) *edges* :test #’equal))

1さらに考えると,変数 bのように生存区間がいくつかに分離している場合には,それぞれの区間ごとに別のレジスタに割り当てることもでき,その方がレジスタ数が少なくてすむ場合がある.この図の場合は実際そうである.ここでは簡単のため区間の分離は扱わない.

2グラフ彩色問題は NP 完全 (計算量のオーダが頂点数N の多項式では表せない)であることが知られていて,完全に解くことは実用的でない.したがってコンパイラでレジスタ彩色を行う場合には経験則に基づいて近似的になるべくよい彩色を効率よく得るよう工夫する.

Page 264: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

264 第 11章 目的コード生成

また,レジスタに置くことができるのは基本型のデータに限るので,各変数のうち

基本型をもつものを is-atomicという述語で調べることができるものとし,リストの

中からそのようなものだけを集める関数を用意する.

(defun filter-nonatomic (l)

(remove-if #’(lambda (x) (not (is-atomic x))) l))

実際に辺を集めるのは次の通り.

(defun collect-intgraph () ; (1)

(for b *blocks* (collect-intgraph-bl b)))

(defun collect-intgraph-bl (b &aux lv x)

(setq lv (filter-nonatomic (block-liveout b))) ; (2)

(maplist #’(lambda (l &aux x) ; (3)

(for x (cdr l) (push-inf-pair (car l) x))) lv)

(for s (reverse (block-code b)) ; (4)

(if (setq d (stat-def s)) (setq lv (remove (car d) lv))) ; (5)

(for x (filter-nonatomic (stat-ref s)) ; (6)

(if (not (member x lv)) ; (7)

(progn (for y lv (push-inf-pair x y)) (push x lv))))))

(1)全ブロックについて,(2)出口で生きている変数を求め,(3)その任意の 2個の対を

辺として追加する.(4)次にブロック出口から逆向きに 1 文ずつ走査しながら以下を実

行する.(5)まず代入されている変数があればそれは生きている変数の集合から外す.

(6)続いてここで参照されている変数について,(7)それがまだ生きている変数の集合

になければ,現在生きている全ての変数との間に辺を張るとともに,生きている変数

の集合にも加える.実行例は次の通り.

>(collect-intgraph)

NIL

>*edges*

((NOTYET T58) (T58 NOTYET) (N T58) (T58 N) (T54 T58) (T58 T54) (T4 T58)

(T58 T4) (I T58) (T58 I) (T6 I) (I T6) (T59 I) (I T59) (T54 I) (I T54)

(T4 I) (I T4) (NOTYET T4) (T4 NOTYET) (N T4) (T4 N) (T7 T4) (T4 T7)

(T6 T4) (T4 T6) (T59 T4) (T4 T59) (T54 T4) (T4 T54) (NOTYET T54)

(T54 NOTYET) (N T54) (T54 N) (T7 T54) (T54 T7) (T6 T54) (T54 T6)

(T59 T54) (T54 T59) (NOTYET T7) (T7 NOTYET) (N T7) (T7 N) (NOTYET T6)

(T6 NOTYET) (N T6) (T6 N) (T7 T6) (T6 T7) (NOTYET T59) (T59 NOTYET)

(N T59) (T59 N) (T7 T59) (T59 T7) (T6 T59) (T59 T6) (NOTYET T11)

Page 265: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.3. レジスタ割当てとレジスタ割付け 265

(T11 NOTYET) (N T11) (T11 N) (T7 T11) (T11 T7) (T6 T11) (T11 T6)

(T59 T11) (T11 T59) (NOTYET T10) (T10 NOTYET) (N T10) (T10 N) (T7 T10)

(T10 T7) (T6 T10) (T10 T6) (T59 T10) (T10 T59) (T11 T10) (T10 T11)

(NOTYET T1) (T1 NOTYET) (N T1) (T1 N) (I T1) (T1 I) (NOTYET I)

(I NOTYET) (I N) (N I) (N NOTYET) (NOTYET N))

以上で辺の集合ができるが,加えて頂点の集合と色 (この場合はレジスタ)の集合が

必要であり,また各頂点についてその度数と彩色した色を属性として保持する.

(defmacro vert-degree (v) ‘(get ,v ’degree))

(defmacro vert-reg (v) ‘(get ,v ’reg))

次に関数 color-graph を示す.引数として利用可能なレジスタの集合を渡すが,

これは実行中は変数*regs*に保持される.また頂点の集合は変数*verts*に入れる.

clolr-graphはまず頂点の集合を作成し,各頂点の度数を計算する.

(defun color-graph (rs)

(setq *regs* rs) (setq *verts* nil)

(for e *edges* (pushnew (car e) *verts*))

(for v *verts* (setf (vert-degree v) 0))

(for e *edges* (incf (vert-degree (car e))))

(try-color (length rs) *verts* *edges*))

実際の彩色は関数 try-colorと try-proceedが行う.try-colorは色の数 k,塗っ

ていない頂点の集合,およびそれらの頂点間を結ぶ辺の集合を受け取る.

(defun try-color (k vs es &aux rs v1)

(for v vs

(if (< (vert-degree v) k) ; (1)

(progn

(try-proceed v k vs es) ; (2)

(for x es (if (eq (car x) v) ; (3)

(pushnew (vert-reg (cadr x)) rs)))

(setf (vert-reg v) ; (4)

(car (set-difference *regs* rs)))

(return-from try-color))

(setq v1 v))) ; (5)

(try-proceed v1 k vs es) ; (6)

(setf (vert-reg v1) nil)) ; (7)

Page 266: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

266 第 11章 目的コード生成

(1)頂点のうちから度数が kより小さいもの vを 1つ見つけ,(2)それを除いたグラフ

を彩色し,(3)vに隣接している頂点の色を全て集め,(4)それ以外の色を任意に 1つ選

んで v に割り当てる.ここで,vの度数は kより小さいので,隣接する全ての頂点と

異なる色が必ず存在することに注意されたい.(5)全頂点の度数が k以上であれば任意

の頂点 v1を選んで (6)これを除いたグラフを彩色し,(7)v1はレジスタへの割当てを

あきらめその印として nilを設定する.try-proceedは次の通り.

(defun try-proceed (v k vs es &aux vs1 es1 res)

(for x es (if (eq (car x) v) (decf (vert-degree (cadr x))))) ; (1)

(setq es1 (remove-if #’(lambda(x) (or (eq (car x) v)

(eq (cadr x) v))) es)) ; (2)

(setq vs1 (remove v vs)) ; (3)

(if vs1 (try-color k vs1 es1)) ; (4)

(for x es (if (eq (car x) v) (incf (vert-degree (cadr x)))))) ; (5)

(1)vに隣接する頂点の度数を全て 1減らし,(2)vから出ている全ての辺をリストから

取り除き,(3)頂点のリストから vを取り除き,(4)もしまだグラフが空でない (塗る

べき頂点が残っている)ならば try-colorを再帰的に呼び出して塗り,(5)グラフの情

報を復元しておきたいので,さっき減らした度数を全て元に戻す.これを実行させた

結果を示す.

>(color-graph ’(d1 d2 d3 d4 d5 d6 d7))

NIL

>(mapcar #’(lambda (x) (list x (vert-reg x))) *verts*)

((T1 D2) (T10 D5) (T11 D4) (T7 D6) (T59 D7) (T6 D2) (I D6) (T4 D5)

(T54 D4) (N D3) (T58 D2) (NOTYET D1))

もしレジスタが不足して主記憶に変数を置いた場合,その変数はスピル (spill — 漏

洩ともいう)されたという.ここではスピルされた変数はない.この割当てを使用して

組単位のコード生成を行った場合の目的コードは図 11.3のようになる.なお,最初の

方にある「movl a6@(8),d3」はパラメタとして渡される変数 nを割り当てられたレジ

スタに載せるためのものである.これを図 11.1と比べると,レジスタ上の値には直接

演算が施せるため無駄なコードが減ったのがわかる.しかし依然として mov d5,d5の

ように効果のない命令も生成されている.これは (ADD T4 T54 T10)に対応する 2命

令の前半であるが,T4はこの命令の参照が最後であり,一方 T10はこの命令から生き

る.このため生きている範囲がちょうど排他的になり,同じレジスタが割り当てられ

Page 267: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.3. レジスタ割当てとレジスタ割付け 267

.text subl #1,d2 cmpl d4,d2

.globl _bubble cmpl d2,d6 ble B7

_bubble: bgt B8 B6:

link a6,#0 B5: movl d7,a0

addl #-48,sp movl d6,d4 movl d4,a0@

movl a6@(8),d3 mulsl #4,d4 movl d5,a0

B1: movl #_a+0,d5 movl d2,a0@

movl #1,d1 movl #_a+-4,d2 movl #1,d1

B11: movl d2,d7 B7:

B2: addl d4,d7 movl d6,d6

cmpl #0,d1 movl d7,a0 jmp B4

beq B9 movl a0@,d2 B8:

B3: movl d6,d6 jmp B2

movl #0,d1 addl #1,d6 B9:

movl #1,d6 movl d5,d5 unlk a6

B10: addl d4,d5 rts

B4: movl d5,a0

movl d3,d2 movl a0@,d4

図 11.3: レジスタ割当てを行なった目的コード

たものである.この種の無駄の改良はケース分析を注意深く行うことによっても可能

だが,むしろコード生成後に除き穴最適化 (後述)を用いる方が一般的である.

ところで,ここまでに示したのはあくまでもレジスタ割付けの基本原理であり,実

際のコンパイラでは次のような点まで考慮する必要がある.

• 彩色アルゴリズムの運用— 一般にレジスタ彩色アルゴリズムでは,次の部分に

おいて複数の選択肢が生じる.

(a) 度数が k未満の節が複数あったときどれを選ぶか

(b) 彩色の時複数の色が使えるなら,そのうちどの色で塗るか

(c) 全ての節の度数が k以上のとき取り除く節の選択

(d) 節を取り除いた後の処置

原理的には実行速度に寄与する度合の高い変数から優先的にレジスタを割り当て

るのが望ましいが,それをどう実現するかについては,寄与の度合の見積りや割

当てアルゴリズムの運用において様々な方法が可能である.また,色が塗り切れ

Page 268: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

268 第 11章 目的コード生成

ないとわかったときここではどれかの節を取り除いて先へ進んだが,厳密にはそ

の節を取り除いた状態で最初から彩色し直す方がよい結果を得る.

• スピルの処置— レジスタに載り切らない変数に対して,単純に全体としてスピ

ルするよりも,他の変数と衝突する最小限の区間のみスピルし,その区間の出口

でレジスタに再ロードする方がよい.その場合にも衝突しているどちら側をどう

スピルするかで複数の選択肢がある.

• レジスタの不均一性— 全てのレジスタが平等な命令セットアーキテクチャはま

れであり,実際には各種の制約がある.例えば浮動小数点データが整数やアドレ

スと別のレジスタ群を使用するなら,レジスタ割当てもこれらを別個に扱う必要

がある.MC68020ではアドレスレジスタとデータレジスタが分離していて前者

は演算に有利,後者はアドレス計算と参照に有利である.そのため T59や T10な

どはアドレスレジスタに割り当てた方がよい.また一方の群が余っていて他方が

足りないとき融通することも考える必要がある.データの種類によってはレジス

タを組にして使用する場合,さらにそのレジスタが隣接した番号 (さらには偶数

番号と奇数番号の対)でなければならない場合などもある.

11.4 目的コードの改良とのぞき穴最適化

組単位のコード生成がもつ弱点である,中間コード命令間の情報流通の欠如を補う 1

つの方法は,生成された目的コードに対して改良を試みることである.のぞき穴最適

化 (peephole optimization)は目的コードの最適化においてよく用いられる枠組の 1つ

である.この手法では,1~2命令からせいぜい数命令程度の「のぞき穴」を用い,目

的コードを順にそののぞき穴を通して見て行き,改良できるパターンが見つかったら

対応する置換えを行う.のぞき穴最適化は目的コードのみを対象とするわけではない

が,目的コード生成後でなければ発見できないような改良を施しやすく,比較的記述

が容易で目的機械ごとに開発する負担が小さいことから目的コードの改良に用いられ

る場合が多い.のぞき穴最適化による目的コード改良の可能性として次のものがあげ

られる (中間コード上で同様の最適化を行うならそのようなケースは残っていないもの

も含めた).

Page 269: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.4. 目的コードの改良とのぞき穴最適化 269

• 無効果命令の削除— 同じレジスタ間での転送のように,何ら効果をもたない命

令を削除する.一般には,複数の命令列を見た結果その中に効果をもたない命令

が発見できる場合が多い (例えばあるレジスタへの転送命令の後で同一レジスタ

を書き換える命令があれば,結果的に前の命令は不要である).

• ロードストア最適化 — レジスタからある変数へのストア命令と同じ変数への

ロード命令が隣接していたら,ロード命令をレジスタ間転送命令に置き換える.

同一レジスタならロード命令を削除する.

• 定数畳み込み— 演算命令の両オペランドが定数のときは結果をあらかじめ計算

しておきそれをロードするように置き換えることを指す.畳み込みの結果条件分

岐が無条件分岐に置き換えられるかもしれない.

• 代数的等価 — x ∗ 2を加算,x ∗ 2n を左シフト,x+ 0を xで置き換えるような

ものを言う.目的コードレベルで行う場合には,どのような計算までならどう置

き換えた方がよい,という判断を機械依存で行えるという利点がある.

• 命令選択 — 1足すのに加算命令の代りに増加命令を使ったり,隣接する命令の

自動加算 (autoincrement)モードを利用する,連続した主記憶番地からのロード

を倍語長のロード 1個に置き換える,など.

• アドレシングモード選択— 間接ロードと演算を演算命令のオペランドの間接参

照モードですませるなど.

• 不到達コードの削除— 無条件分岐の次の命令がラベルでないとき,次のラベル

までのコードは実行されようがないので削除する.

• 分岐最適化— 分岐と行き先ラベルが隣接している場合には両者を削除する.分

岐の行き先がまた無条件分岐ならもとの分岐の行き先を最終的な行き先に変更す

る.無条件分岐を飛び越す条件分岐は条件を反転して 1つの条件分岐にする.

これらは互いに関連している.例えば分岐最適化により行き先として使用されないラ

ベルができ,その結果そこから次のラベルまでが不到達コードになるかもしれない.

Page 270: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

270 第 11章 目的コード生成

11.5 コード生成器生成系

11.5.1 記述主導型コード生成

目的コード生成が面倒な理由の 1つは,ある動作を実現するための命令列が (命令コー

ドやアドレシングモードの選択を含めると)非常に多く存在し,それらを適切に選択す

るためのケース分析に手間がかかるという点にある.しかも,ケース分析を徹底させ

るほどその手順が複雑になり,各命令ごとに全てのケースをカバーしたテストを徹底

しないと虫を残しやすいなどの問題も生じる.そして,複雑な機能をもつ命令を多く

利用しようとするほど,多くのケース分析が必要になる.

この問題に対する回答の 1つとして「こういう計算のためにはこういう命令を使え

ばよい」という記述に基づいてコード生成を行うことが考えられる.これを記述主導

型コード生成 (description driven code generation)とよび,それを実現するソフトウェ

ア群をコード生成器生成系 (code generator generators)とよぶ.以下ではその代表的

なものを取り上げて解説する.

11.5.2 木のパターンマッチによるコード生成

記述主導型コード生成の素朴な形として,木の書換え (tree rewriting)に基づくコード

生成が存在する.コード生成の入力となる木としては,抽象構文木そのままではなく,

番地計算や (レジスタ割当てがすんでいれば)レジスタ名などが直接表れるもの (低レ

ベル中間木 — low-level intermediate tree)を使用する.低レベル中間木は抽象構文木

から直接つくることもできるし,最適化後の 4つ組をもとに組み立てることもできる.

後者の場合は,基本ブロック内で一度定義され参照されるだけのテンポラリを木の中

間節とし,それ以外のテンポラリは変数と同様に扱う.ただしごく小さな共通式部分

式で,毎回テンポラリからもってくるのと他の値から計算するので手間が変わらない

場合にはその部分を重複させてもかまわない.例えばブロックB5の前半部分に対応す

る低レベル中間木 (レジスタは割り当てていない)を図 11.4に示す.

木のパターンマッチによるコード生成では,木の中に特定のパターンが表れたらそ

れを別のパターンで置き換えて木を簡約するとともに,その簡約に対応した計算を行

うコードを出力する.これを制御するためには,

Page 271: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.5. コード生成器生成系 271

:=

T6 deref

+

addr

A -4

*

I 4

図 11.4: 低レベル中間木の例

(パターン 置換 出力コード)

の組を複数用意し,コード生成器はこの記述を参照しながらコードを生成していく.ま

ず,木に表れる諸成分の「種類」と「変数であるか否か」の情報をアクセスするマク

ロと,リストの各要素についてこれらを設定する関数を用意する.

(defmacro is-var (x) ‘(get ,x ’is-var))

(defmacro kind (z) ‘(get ,z ’kind))

(defun dcl-kind (k l)

(for x l (setf (kind x) k)))

(defun dcl-vars (k l)

(for x l (setf (kind x) k) (setf (is-var x) t)))

(setq *dregs* ’(d0 d1 d2 d3 d4 d5 d6 d7))

(setq *aregs* ’(a0 a1 a2 a3 a4 a5 a6 a7))

(dcl-kind ’dreg *dregs*)

(dcl-kind ’areg *aregs*)

(dcl-vars ’num ’(*num *num1 *num2 *num3))

(dcl-vars ’lvar ’(*lvar))

(dcl-vars ’gvar ’(*gvar))

(dcl-vars ’areg ’(*areg))

(dcl-vars ’dreg ’(*dreg *dreg1 *dreg2))

(dcl-kind ’gvar ’(a))

(dcl-kind ’lvar ’(t6 i))

コード生成を制御する 3つ組は変数*rules*に入れておく.ここでは生成コードが

新たに使用するレジスタの集合と不要になって返却するレジスタの集合を加えた 5つ

組とした.例えば最初の規則は「数値の節をデータレジスタの節に書き換えるととも

Page 272: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

272 第 11章 目的コード生成

に,定数をデータレジスタにロードする movl命令を生成する.ただしそのデータレジ

スタは新たに割り当て,返却するレジスタはない」のように読む.

(setq *rules*

’((*num *dreg ("~% movl #~S,~S" *num *dreg) (*dreg) ())

(*lvar *dreg ("~% movl a6@(~S),~S" (var-off *lvar) *dreg) (*dreg) ())

((addr *gvar *num) *areg ("~% lea ~S+~S,~S" *gvar *num *areg) (*areg) ())

((+ *areg *dreg) *areg ("~% adda ~S,~S" *dreg *areg) () (*dreg))

((* *dreg1 *dreg2) *dreg1 ("~% muls ~S,~S" *dreg2 *dreg1) () (*dreg2))

((deref *areg) *dreg ("~% movl ~S@,~S" *areg *dreg) (*dreg) (*areg))

((:= *lvar *dreg) t

("~% movl ~S,a6@(~S)" *dreg (var-off *lvar)) () (*dreg))))

*

*num *dreg

movl #num,dreg

*lvar *dreg

movl a6@(...),dreg

*gvar *areg

lea gvar+num,areg

addr

*num

*dreg1 *dreg1

muls dreg2,dreg1

*dreg2

:=

*lvar

movl dreg,a6@(...)

*dreg

nil

図 11.5: コード生成用規則の例

これらを図で表現したものを図 11.5に示す.実際にコード生成を行う関数は次の通り.

(defun tree-rewrite (tree)

(while (not (symbolp tree))

(print tree) (setq tree (try-rewrite tree)))

tree)

木を繰り返し書き換え,1つの記号 (ここでは最後までコード生成ができたら tさも

なければ nil)になったら終りとする.実際の書換えは次の通り.

(defun try-rewrite (tree &aux t1)

Page 273: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.5. コード生成器生成系 273

(for r *rules* ; (1)

(if (try-match tree (car r)) ; (2)

(progn (alloc-reg (cadddr r)) ; (3)

(apply #’format (cons t (mapcar #’eval (caddr r))))

(free-reg (caddddr r))

(return-from try-rewrite (eval (cadr r))))))

(cond ((atom tree) nil) ; (4)

((and (cddr tree) (setq t1 (try-rewrite (caddr tree)))) ; (5)

(cons (car tree) (cons (cadr tree) (cons t1 (cdddr tree)))))

((setq t1 (try-rewrite (cadr tree))) ; (6)

(cons (car tree) (cons t1 (cddr tree))))

(t nil))) ; (7)

(1)各変換規則を順に調べ,(2)この木にマッチするものがあれば,(3) 新しく作業レ

ジスタを割り当てる必要があれば割り当て,規則に記されているコードを生成し,参

照し終ったレジスタを返却し,書き換えた木を値として返す.ここで evalが使われて

いるのは,tree-matchにおいてパターン中の変数が部分木にマッチしたときにはその

変数 (記号)の値としてマッチした部分木をセットするようになっているため,その値

を参照するものである.なお,パターンの中に表れない,種別がレジスタの変数には

alloc-regで具体的なレジスタ名がセットされる.(4)この節全体とマッチする規則が

なかった場合には,木の葉であれば失敗,(5)そうでなければ木が子供をもつのでまず

右側,(6) それでだめなら左側の子供を書き換えることを試み,成功すればその部分を

書き換えられたもので置き換えた木を返す.(7)どちらも失敗したら全体としても失敗

を返す (7).パターンマッチを行う関数 try-matchは次の通り.

(defun try-match (x y)

(cond ((and (atom x) (atom y)) (try-match-atoms x y))

((or (atom x) (atom y) (/= (length x) (length y))) nil)

(t (not (member nil (mapcar #’try-match x y))))))

まず両方が葉であれば try-match-atomsに頼む.そうでなければ,両方の子供の数が

同じでなければ失敗.同じであれば,各要素ごとにマッチさせ,1箇所でも失敗すれば

全体として失敗,そうでなければ成功を返す.try-match-atomsは次の通り.

(defun try-match-atoms (x y)

(cond ((eql x y) t)

((numberp y) nil)

((numberp x) (and (is-var y) (eq (kind y) ’num) (set y x)))

Page 274: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

274 第 11章 目的コード生成

((is-var y) (and (eq (kind x) (kind y)) (set y x)))

(t nil)))

x(木の側)と y(パターン側)が同じ値なら成功.そうでなくて,yが数値なら (木の側

も同じ数値でない限りマッチしないわけなので)失敗.xが数値で yが数値に対応する

変数なら成功.それ以外の場合 xと yの種類が同じで yが変数なら成功 (これらの場

合は変数の値として xの値をセットする).それ以外は失敗.あと残っているのはレジ

スタを割り当てたり返却したりする操作である.

(defun alloc-reg (l)

(for r l

(set r (if (eq (kind r) ’dreg) (pop *dregs*) (pop *aregs*)))))

(defun free-reg (l)

(for r l (if (eq (kind r) ’dreg)

(push (symbol-value r) *dregs*)

(push (symbol-value r) *aregs*))))

これらは,単に渡されたリストの各要素に対し,その種類に応じてデータレジスタ,

アドレスレジスタを割り当て/返却するだけである.次に実行例を示す.

>(tree-rewrite ’(:= t6 (deref (+ (addr a -4) (* i 4)))))

(:= T6 (DEREF (+ (ADDR A -4) (* I 4))))

movl #4,D0

(:= T6 (DEREF (+ (ADDR A -4) (* I D0))))

movl a6@(-4),D1

(:= T6 (DEREF (+ (ADDR A -4) (* D1 D0))))

muls D0,D1

(:= T6 (DEREF (+ (ADDR A -4) D1)))

lea A+-4,A0

(:= T6 (DEREF (+ A0 D1)))

adda D1,A0

(:= T6 (DEREF A0))

movl A0@,D1

(:= T6 D1)

movl D1,a6@(-20)

T

このように,1回の書換えごとに木が簡約され,それに対応してコードが生成され

ていく様子がわかる.ただし,ここでは「局所変数があればデータレジスタにロード

Page 275: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.5. コード生成器生成系 275

する」「定数があればデータレジスタにロードする」など比較的簡単な規則しか与えて

いないので,常にレジスタにもってきてレジスタ間演算を行うコードになる.ここで

「局所変数とレジスタをかけて結果をレジスタに置く」「アドレスレジスタにデータレ

ジスタの値を加えた分だけ先の番地の内容を参照する」などの命令に対応する次の記

述を追加する.

((* *lvar *dreg) *dreg

("~% muls a6@(~S),~S" (var-off *lvar) *dreg) () ())

((deref (+ *areg *dreg)) *dreg

("~% movl ~S@(~S),~S" *areg *dreg *dreg) () (*areg))

この状態で実行させると,今度は次のようになる.

(:= T6 (DEREF (+ (ADDR A -4) (* I 4))))

movl #4,D0

(:= T6 (DEREF (+ (ADDR A -4) (* I D0))))

muls a6@(-4),D0

(:= T6 (DEREF (+ (ADDR A -4) D0)))

lea A+-4,A0

(:= T6 (DEREF (+ A0 D0)))

movl A0@(D0),D0

(:= T6 D0)

movl D0,a6@(-20)

T

このように,単に記述を追加するだけで場合分けを増やせること,および具体的な

命令の形式などに関する情報をコード生成器から分離できることが記述主導型コード

生成の利点である.一方,木のパターンマッチに基づくコード生成器では次の点が問

題になる.

• マッチするパターンがなくて,書換えが止まってしまう可能性.

• 書換えのループが起きて止まらなくなる可能性.

• 複数のパターンがマッチしたときの選択方式.

最初の問題の例としては,「乗算命令はメモリ上の 2つの値しかオペランドにできず,

片方の値がレジスタにあるときはいったんメモリに格納しなければならない」という

変ったアーキテクチャの場合だと「レジスタの葉をテンポラリ変数の葉に書き換える」

Page 276: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

276 第 11章 目的コード生成

という規則がないと乗算のところで書換えが止まってしまう.一方,そのような規則

を追加した場合には 1つの節を「テンポラリ→レジスタ→テンポラリ→…」のように

繰り返し書き換えて止まらなくなる可能性が生じる.さらに,ある経路を通ればうま

くコードが生成できるが別の経路だと行きどまりになるかもしれない.そしてコード

生成である以上できるだけ効率の良いコードを選択する必要もある.

これらに対しては,パターンに優先順位をつけたり,ある規則が必要とする形にな

るように別の規則での書換えを制御するなどの対応が考えられるが,基本的には記述

群一式ごとに解決しなければならない問題であり,その意味で「虫のない」コード生

成記述を用意する作業は必ずしも簡単でない.

11.5.3 Graham-Granvilleコード生成器

木の書換えによるコード生成器は直感的にはわかりやすいが,上記の問題点に加え,パ

ターンマッチが遅いという問題もある.これに対し,GranvilleとGrahamはパターン

マッチを LR構文解析に置き換えることにより効率的にコード生成を行う手法を提案

した.この手法では,まず木を前置記法 (prefix notation)で書き表す.例えば図 11.4

の木であれば,

:= t6 deref + addr a -4 * i 4

となる.すると,木の書換えはこの列の上での置換えに相当する.書換えでは 1個以上

の節からなる部分木を 1個の節に置換えるので,見方を替えればこれは書換え規則群

を文脈自由文法と考え (ただしパターン側が生成規則の右辺,置換え側が左辺になる),

前置記法の列を還元していって最後に 1個の記号にすることに対応する.そして,文

脈自由文法に従って列を還元する LR構文解析によって行う,というのが基本的アイ

デアである.例えば前節の書換え規則群を文法+動作の形で記すと次のようになる.

dreg → num { " movl #num,dreg" } ; R1

areg → addr gvar num { " lea gvar+num,areg" } ; R2

dreg → * lvar dreg { " muls a6@(off[lvar]),dreg" } ; R3

dreg → deref + areg dreg { " movl areg@(dreg),dreg" } ; R4

t → := lvar dreg { " movl dreg,a6@(off[lvar])" } ; R5

この文法によって先の前置記法コードは次のように還元でき,合せてコードが生成で

きる.

Page 277: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.5. コード生成器生成系 277

:= t6 deref + addr a -4 * i 4

→ := t6 deref A0 * i -4 (R2) lea A+-4,A0

→ := t6 deref A0 * i D0 (R1) movl #4,D0

→ := t6 deref A0 D0 (R3) muls a6@(-4),D0

→ := t6 D0 (R4) movl A0@(D0),D0

→ T (R5) movl D0,a6@(-20)

原理は以上であるが,ただし「コード生成文法」はプログラミング言語の文法と異

なる性質をもつ.まず生成コード列に複数の可能性があることに対応して文法に多く

の曖昧さが含まれ,多くのシフト-還元衝突や還元-還元衝突が生じる.前者については

基本的にシフトを行うが,これは必要になるまではコードを生成しない (高機能の命

令を選ぶ)ことに相当する.還元-還元衝突に対しては,どれを選ぶか別途決定する規

則を設け,還元を行ってよいかどうかを指定する検査を付随させる.これは「結果を

入れようとしているレジスタは書き換えてもよいか」「この足そうとしている定数は 0

か」など,構文だけでは表せない条件を記述するのに必要である.

Graham-Granvilleコード生成器の利点としては,LR解析器を利用するため効率的

なこと,書換えが行き詰まる場合を解析表生成時に検出できることなどがあげられる.

一方,弱点としては LR解析器ではパターンマッチの順序が左から右に固定されてい

ること,解析表が非常に大きくなる可能性があること,原理的に構文的であり,基本

的に意味情報を扱わないこと,などがある.特に最後の点は,現実には命令に関する

構文的に表せない制約 (レジスタ対や特別な役割りをもつレジスタなど)が多く存在す

るため,これを統一的に扱えるようにすることが望ましい.

11.5.4 Davidson-Fraserコード生成器

ここまでに述べた記述主導型コード生成の方式では,木ないしそれに対応する中間コー

ドと目的コードの対応を記述していた.したがって,高機能な命令を活用しようとす

るほど複雑なパターンを書く必要が生じ,また 1つの命令が複数の目的に使える場合

にはそれぞれの場合ごとに記述を必要とするという弱点をもっていた.これに対し,

Davidsonと Fraserが提唱したやり方では RTL (register transfer language,レジス

タ転送言語)とよばれるものを中間コードと目的機械記述の双方に使用する.RTLは

見た目には低レベルの 4つ組コードに似ているが,作業場所としてテンポラリの代り

に仮想レジスタを使用する (仮想レジスタはその名の通りレジスタであるが,ただし無

Page 278: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

278 第 11章 目的コード生成

限個数使える仮想的なものであり,後で実際のレジスタに割り当てられる).目的機械

記述はハードウェアにどのような命令が存在するかを RTLとアセンブリ言語の対応表

の形で記述する.例えば addl Ax@(8),Dyという命令は

r[y] := m[r[x] + 8] + r[y]

という RTL表現に対応する (ただし r[n]は仮想レジスタ参照,m[m]はメモリ参照

を表す).すなわち,RTLは低レベルなので 1 つの機械命令の効果はいくつかのRTL

式を組み合せて表現することになる.

一方,翻訳過程においては,いずれかの段階で RTLによる中間コードを生成する.

これは最適化ずみの各種中間コードからRTLを生成しても,抽象構文木から直接RTL

を生成してデータフロー解析や最適化を RTLのうえで行うのでもよい.いずれにせよ,

RTL生成の段階ではケース分析は行わず,単純なコード生成を行う.そしてケース分

析の代りに接合器 (combiner)とよばれるパスが存在し,ここで物理的および論理的に

隣接する n個のRTL命令列全てについて,それらの命令を組み合せたのと同じ効果を

もつ機械語命令が存在するかどうかを機械記述に照らして調べ,存在するならその命

令列を 1個の RTL命令としてまとめたものに置換する.これを反復することにより,

高機能な命令に対応する RTL命令が組み立てられ,結果的に高機能な機械命令が活用

できる.この方法を取ることにより得られる利点として次のものがあげられる.

• RTLの構造は低レベルで単純であり,生成するのも扱うのも容易である.

• RTLの形式自体は特定の目的機械に依存しないものであり,したがって機械独

立なコード生成系生成器を実現している.

• 目的機械の記述として「どのような命令が存在するか」を与えるので,何を記述

すればよいかが明確である.

• 「置き換えることのできる (高機能命令に対応する)パターン」を記述するので

はないので,記述の複雑さが小さい.

• 命令の接合可能性はあらゆる場合が機械的に検査されるので,特定のパターンを

書き忘れたためその場合だけ高機能な命令が使われないなどの偏りが生じない.

• RTLは 4つ組と同様順序の入替えが容易なので,命令列が物理的に隣接してい

なくても論理的に隣接していれば接合させられる.

Page 279: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.5. コード生成器生成系 279

RTL命令の接合はパターンマッチを伴うので遅そうであるが,実現上はあらかじめ全

ての n(実用上は 2~3程度)命令の組について接合の可能性を調べて表を作成すること

で高速化できる.例として,広域配列 a[i] の値を参照するRTL命令列を考えてみる.

ここではMC68020のハードウェアレジスタ d0~d7を r[0]~r[7],a0~a7を r[8]

~r[15]と考え,それより大きい番号を仮想レジスタと考える.したがってフレーム

ポインタは r[14]となる.

*r[16] := r[14] + -4 -- 局所変数 iの番地*r[17] := m[r[16]] -- iの値*r[18] := r[17] * 4

r[19] := a - 4 -- 配列 aの先頭マイナス 4番地*r[20] := r[18] + r[19] -- a[i]の番地r[21] := m[r[20]] -- a[i]の値

このRTL命令列はごく基本的な命令のみを使用しているので,生成する際のケース

分析も簡単なことは明らかであろう.次に接合器によって命令列を短縮していく.ま

ず上で*のついている 4命令が 2命令ずつ接合される.

r[17] := m[r[14] + -4] -- movl a6@(-4),rx に相当r[19] := a - 4

r[20] := (r[17] * 4) + r[19] -- lea ax@(0,rx:l:4),rx に相当r[21] := m[r[22]]

ここでさらに最後の 2つが接合できて次のようになる.

r[17] := m[r[14] + -4]

r[19] := a - 4

r[21] := m[(r[17] * 4) + r[19]] -- movl ax@(0,rx:l:4),rx に相当

このように,目的機械の複雑なアドレシングモードや多数の機能を合せもった命令

を単純な命令の接合により活用できること,そしてその記法自体が対象機械に依存し

ないことが Davidson-Fraserコード生成器の特徴である.

この手法を用いたコンパイラとして GNU C Compiler (gcc)があげられる.その配

布キットには 20種ほどの異なる命令セットアーキテクチャに対応する記述が付随して

おり,そのどれかを選択することで各アーキテクチャ用の目的コードを出すコンパイ

ラが得られる.gccでは中間コード自体も全面的にRTL (ただし形式は Lispの S式ふ

うとなっている)を採用している.

Page 280: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

280 第 11章 目的コード生成

load y

load z

load b

load c

*

+(1)

load u

+(2)

+(3)

load b

load c

*load y

load z

+(1)

load u

+(3)

+(2)

図 11.6: パイプラインスケジューリングの概念

11.6 様々な目的機械の特性への対処

11.6.1 命令スケジューリング

最適化に際して「実行時間の短縮」を考慮するのに,ここまでは単純に命令数を減ら

し,また個々の命令として実行時間が短い命令を選択することを目指していた.言い

換えれば,プログラムの実行時間は各命令の実行時間の総和であり,同じ命令であれ

ばその実行時間は命令列のどの場所にあっても同じであることを暗黙のうちに前提と

していた.しかし,現在の計算機システムの大部分においては,もはやこの前提は成

り立たない.というのは,これらの計算機では命令の実行を高速化するためパイプラ

イン (pipeline)実行方式を採用しているためである.パイプライン方式ではある命令

の実行と次の命令の実行は部分的にオーバラップしており,そのため命令どうしの干

渉の有無で各命令の実行完了までに要する時間が変化する.言い換えれば,コンパイ

ラができるだけ干渉が少ないように命令列を並べることで,目的コードの実行時間を

さらに短縮できる可能性がある.このように目的機械の命令実行タイミングを考慮し

て命令列の並べ方を調整することを命令スケジューリング (instruction scheduling)と

よぶ.例えば次のコード列を考える.

x := y + z; a := b * c; t := a + u + x;

ここでオペランドが全て主記憶にあるとすると,これらのオペランドを転送してく

るのにも,また加算や乗算などの演算にも時間がかかる.これを素直な順序で計算す

Page 281: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.6. 様々な目的機械の特性への対処 281

るコードは例えば図 11.6左のタイミングで命令を実行することになる.すなわち,パ

イプライン方式ではある命令が完了する前に次の命令が発行 (issue)されるが,たとえ

ば加算命令 (1)では当然その両オペランドが揃うまで演算が開始できないので,斜線

部分で示したように命令の実行が待たされる.ここで一番ネックになっているのは乗

算命令なので,この計算を優先して実行し,u + xの加算も (言語仕様が許すなら)先

に行うように命令の順序を入れ替えると,図 11.6右のように命令実行所要時間を大幅

に短縮できる.

このようなスケジューリングを系統的に行うには,ブロック内部の命令列を依存関係

に基づいて並べた非循環有向グラフを構成し,最も実行時間がかかる際重要経路 (clitical

path)の命令から順に生成していくやり方が可能である.

11.6.2 RISCアーキテクチャ

RISC(reduced instruction set computer)とよばれる種類の計算機では,分岐命令の次

にある命令は分岐が起きる場合でも分岐先の命令に先だって必ず実行される (遅延分岐

— delayed branch),主記憶からレジスタへのロード命令はその直後の命令に対しては

効果をもたない (遅延ロード — delayed load),などの特性をもつ命令体系をもつもの

がある.これらの特性がある場合には,当然コンパイラにおいてそのことを考慮した

命令列を生成しなければ正しく動作しない.また単に正しく動作するだけでなく,こ

れらの特性をうまく活かした命令列を生成することが,特に RISCアーキテクチャ用

のコンパイラに期待されることである 3.

遅延分岐の処理は基本的に分岐の直後 (分岐スロット)に nopでない,役に立つ命令

を置くことをめざす.最初に,分岐より前にある命令の 1つを分岐の直後に移すこと

を考える.移すことができる命令は,移したことによってプログラムの意味が変らな

いような命令であればどれでもよい.分岐の前のブロックが十分長ければそのような

命令が見つかる可能性は大きい.見つからない場合には,分岐の先の命令を移すこと

を考える.まず,無条件分岐命令場合は,その行き先にある命令を移すこと自体につ

いて問題はない.ただし,移した結果,他の経路から来たときにその命令がなくて困

るので,他の経路から来るときはその命令が実行されるように調整する.

3分岐やロードの次に nop 命令 (何もしない命令を)必ず入れてしまえばこれらの特性を考慮しなくてもすむが,明らかに実行速度の点では不利である.

Page 282: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

282 第 11章 目的コード生成

一方,条件分岐のときには,分岐が成立する場合としない場合の2つの「行き先」が

あるので,そのどちらの命令を移すかを決めなければならない.当然,より大きい頻

度で実行される側 (例えばループ終了判定の場合なら終了しない側)を選択するべきだ

が,どちらとも決められない場合には適当にどちらかを選ぶ.問題は,選ばなかった側

の枝についても移した命令が実行されてしまう点である.もし移した命令が (生きてい

る変数を書き換えてしまう等の)害をなさないなら,そのままで問題ない.そうでなけ

れば,別の命令で無害なものを選べないか検討する.選んだ枝が明らかに優先される

というのでなく,逆の枝に都合の良い命令があればそちらを選んでもよい.いずれも

だめなら,移した命令の効果を取り消す命令を生成することを検討する.これらが全

て失敗した場合はあきらめて nopを分岐の後に置くことにする.実際のプログラムで

はほとんどの場合分岐の直後に有効な命令を移せることが報告されている.遅延ロー

ドの場合もロード命令の直後にロードの結果を参照しないような適切な命令をもって

くる,という点で同様である (分岐と違って行き先が複数あったりしないのでむしろ扱

いはやさしい).

RISCアーキテクチャに多く見られる他の特徴として,演算命令がレジスタ間のみ

であり,主記憶を参照するのは原則としてロード/ストア命令のみである (ロードスト

アアーキテクチャ),アドレスモードや命令形式が少ない,レジスタが多数ある,など

の点があげられるが,これらはどれもここまでに述べてきた技法で扱うのに問題ない.

レジスタについては一部の機械では「レジスタ窓」(register window)とよばれる機構

を採用している.

これは図 11.7にあるように,多数あるレジスタの中から一時には n個のレジスタの

みがアクセスでき,そのアクセスできる範囲 (窓)を「ずらす」機能が備わっていると

いうものである.具体的にはこれはある手続きで使うレジスタの組とそこから呼ばれ

る別の手続きで使うレジスタの組をずらすことで,レジスタの退避回復オーバヘッド

をなくす目的で使われる (呼出しが深くなって機械に備わっているレジスタの数で足り

なくなったときは当然主記憶への退避と回復が起こるが,その頻度は十分小さいとい

うのがこのようなアーキテクチャを採用する人々の主張である).もっとも,呼び側と

呼ばれ側で完全にレジスタが別個だと引数のレジスタ渡しができないので,ある窓と

次の窓を部分的にオーバラップさせてこの範囲にあるレジスタは両方の手続きから (名

前は違うが)アクセスできるようにする.

レジスタ窓に対するコードの生成も,引数が基本的にレジスタ渡しになるという点

Page 283: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.6. 様々な目的機械の特性への対処 283

i0

...i7

i2

l0

...l7

l2

o0

...o7

o2

i0

...i7

i2

l0

...l7

l2

o0

...o7

o2

オーバラップしている範囲

引数受け用

局所レジスタ

引数渡し用

呼び

戻り

図 11.7: レジスタ窓

Page 284: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

284 第 11章 目的コード生成

が特徴である程度で,これまでに述べてきた点と本質的に違うことはない.なお,「受

け用」「渡し用」と書かれたレジスタはそれにしか使えないわけではなく,(「渡し用」

は手続き呼出しをまたがって内容が保証されない点さえ注意すれば)作業レジスタや変

数割当て用に使うことも自由である.

11.6.3 ベクトルプロセッサとマルチプロセッサ

ベクトルプロセッサ (vector processor)とは,1次元配列のように同種のデータが多数

並んだものどうしに対して一斉に同一の演算を施すような命令 (ベクトル演算命令)を

もつ計算機システムをいう.「スーパーコンピュータ」とよばれるシステムの多くは

ベクトルプロセッサに分類できる.さらに近年ではそのようなCPUを複数備えたマル

チプロセッサ (multiprocessor)構成のシステムも多くなっている.

一般的にベクトルプロセッサは配列のような連続データをいったんベクトルレジス

タとよばれる (多数の値の列を保持できる)レジスタにロードし,ベクトル命令とよば

れる一群の命令によりそれらの間で高速に演算を施すことができる.通常の命令の代

りにベクトル命令を使うようにすることをベクトル化 (vectorization)とよぶ.ベクト

ルプロセッサ用のコンパイラはなるべく多くのコードをベクトル化することが望まれ

る.ベクトル化の基本は最内側のループにおける配列演算をベクトル命令に置き換え

ることである.

for i := 1 to n do a[i] := b[i] + c[i];

これはごく素直に「ベクトル bとベクトル cのベクトル加算命令を実行し,結果を

ベクトル aに代入する」になる.ループ内に互いに独立な演算がある場合には同様に

してそれらを個別にベクトル命令に置き換えればよい.問題は

for i := 1 to n do begin

a[i] := b[i] + c[i];

d[i] := a[i] * 2.0

end;

のように互いに命令が独立でない場合である.この場合には独立ではないが,素直に上

のコードに続けて「ベクトル aを 2倍して結果をベクトル dに格納する」ベクトル命

令を出してよい.ハードウェア資源がある限りベクトル演算はチェイニング (chaining)

Page 285: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

11.6. 様々な目的機械の特性への対処 285

されるので,aの最初の要素が生成されるとただちにそれを 2倍して d に入れるプロ

セスも aの残りの計算と並行して動くようになる.一方,2 番目の a[i]が a[x[i]]な

どのようになっていた場合には通常 x[i]の内容を翻訳時に知るのは困難なのでベクト

ル化することはできない.一般に複数の命令間に依存関係があるかどうかを調べるこ

とは依存解析 (dependence analysis)とよび,ベクトル化における主要な問題となって

いる.

また,上の例ではベクトルをスカラー (というのは,ベクトルではない単一の値)倍

するベクトル命令があればいいが,ない場合は 2.0を n個並べた定数ベクトルを用意

してでもベクトル加算命令を使用するべきである.多くのベクトルプロセッサではベ

クトル演算中に条件分岐は指定できないので,ループの中に ifがある場合にはベクト

ル化をあきらめることになる.ただし,ifの条件がループ不変なら ifをループ外に出し

てループを分割する変形の結果ベクトル化できるようになる可能性がある.また,条

件ベクトルとよばれるベクトルの値 (論理値の真偽,あるいは数値の正/負/ゼロ)に応

じて代入を選択的に行える機構をもつ場合には,条件式を条件ベクトルに変換するこ

とで ifを含むループをベクトル化できるかもしれない.

for i := 1 to n do

if a[i] > b[i] then a[i] := a[i] + b[i]

else a[i] := 0.0;

これは例えば次のようになるかもしれない.

1. ベクトル aからベクトル bを引いて条件ベクトル cを生成.

2. ベクトル aとベクトル bを足したベクトルを生成し,ベクトル cが非負のもの

に対応する要素のみベクトル a に格納.

3. 0.0が並んだ定数ベクトルをロードし,ベクトル cが負のものに対応する要素の

みベクトル aに格納.

マルチプロセッサの場合にはこれに加えて,多量のデータをうまく各プロセッサに分

配して演算し,さらに隣接するプロセッサ間で必要な情報を通信したり,プロセッサ

間で演算時間が異なる場合には同期命令を挿入するなどの処置がコンパイラに要求さ

れる.

Page 286: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

286 第 11章 目的コード生成

11.7 練 習 問 題

11-1. 自分の手元にある機械の命令体系を調べ (それにはC言語などで簡単な手続きを

書いて翻訳させアセンブラ出力を眺めるのがよい),納得したら 11.1節の組単位

コード生成を手直して手元の機械のアセンブリコードが生成されるようにせよ.

11-2. 4つ組の代りに問題 9-2で使用したような後置コードに対する組単位コードを生

成を行わせてみよ.

11-3. 問題 11-1および 11-2のコード生成器を解釈実行型コード生成器に改良してみ

よ.11.2節の例のように複雑な状態を扱うと大変なので,「どのテンポラリ (また

は仮想スタック位置)は現在どのハードウェアレジスタに入っている」という状

態だけにしておくとよい.

11-4. 問題 11-1のコード生成器を直して各テンポラリおよび変数がレジスタに割り付

けられているときはレジスタを参照するようにせよ.次に適当な例題プログラム

について手でレジスタ割付けを行ったうえでコードを生成させ,レジスタ割付け

を行わない場合と実行速度を比較せよ.または,計算機にレジスタ彩色を行わせ

た場合と手で割り付けた場合との比較でもよい.

11-5. 手元に gccなどコード生成器生成系の技術を活用したコンパイラがあれば,そ

の目的機械記述などを探求してみよ.gccであれば最適化やコード生成の各段階

における中間コードを書き出す機能もついているので,その出力を観察するのも

面白い.

11-6. あまり長くないプログラムを翻訳し,アセンブリコードになった状態で命令の

順序などを (プログラムの意味が変らない範囲で)変更してその実行時間に対す

る影響を調べよ.例えば遅延分岐をもつ命令セットの場合,遅延スロットを nop

命令にしておくのときちんと埋めるのとではどれくらい違うかなども試してみよ.

Page 287: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

287

付 録A Lispプログラムの読み方

本書には多数の CommonLispプログラムが出てくる.本書の読者は手続き型言語によ

るプログラミングについての知識はもっているものと考えているが,Lisp言語につい

ては始めてであるかもしれない.そのような読者のために CommonLisp言語プログラ

ムの読み方について,簡潔に解説する.簡潔さを優先したため,本書に出てこない概

念については説明を省いている.より詳しい言語仕様について知りたい読者は参考文

献などを参照されたい.

アトムとリスト

Lispの世界においては,普通の言語における「名前」のことを「記号」(symbol)とよ

ぶ.記号は関数名や変数名であることもできるが,それ自体がデータとして扱われる

こともできる.次のものは記号の例である.

this abc x+123 that-is

CommonLispでは記号中の英字は基本的に大文字に変換される.特別な記号として

tと nilがある.tは「真」,nilは「偽」を表すのに用いられる.記号の他に次のよ

うな基本データ型がある.

#\a #\1 #\A --- 文字"" "ABC" "This" --- 文字列0 123 -45 --- 数値

記号やこれらの基本データ型の値をアトム (atom)とよぶ.アトムを材料として,そ

れを「()」でくくって組み立てたものがリストである.次のものはアトムから成るリ

ストである.

Page 288: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

288 付録 A LISPプログラムの読み方

(this 1 #\A "that")

リストの各要素は空白,タブ,改行などで区切る.リストの要素はまたリストであっ

てもよい.次のものはリストとアトム両方を含むリストである.

((a b) c d)

アトム,リストの両方を合せて S式 (S-expression)とよぶ.なお,空なリスト ()は

nilと同じものである.

値 と 評 価

Lispの世界では S式の値を計算することを「評価」(evaluation)とよぶ.S式の値を評

価するときは次のような規則に従う.

• S式が記号のとき — その記号に束縛 (bind)されている値を結果とする.束縛さ

れている,というのは「値が入っている」という意味だと考えればよい.ただし

tや nilを評価した結果はそれぞれ tと nilになる.

• S式が記号以外のアトムのとき — 評価した結果は元のアトムそのものである.

• S式がリストのとき — リストは「(操作名 引数 1 … 引数 n)」の形をしている

とみなされ,結果は操作によって異なる.例えば操作が関数であれば,結果は関

数に引数を与えて実行した結果となる.操作名としてはほかにマクロ名,特殊形

式名がある.

Lispの処理系は,入力した S式を評価し,その結果の値を打ち出すことを繰り返す.

例えば次のような具合である.

> (+ 1 2 3)

6

> (- (+ 5 4) 2)

7

なお,関数の引数自身もまず評価されていることに注意.このように,リストを渡

すとそれは上の規則に従って評価されてしまうので,データとしてリスト (や記号)を

渡すときには「この先はデータだから評価しないで」という印が必要になる.そのた

めには特殊形式 quoteを用いる.

Page 289: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 A LISPプログラムの読み方 289

car cdr

car部

cdr部

図 A.1: consセル

A B C

nil(A B C)

A B

nilC D

nil

((A B) C D)

A B C

D(A B C . D)

図 A.2: リストの構造

> (quote (+ 5 4))

(+ 5 4)

これは非常に多く使われるので,「(quote S式)」の代りに「’S式」と書いても同様

に展開されるようになっている.

リスト操作関数

Lispの内部ではリストは「consセル」とよばれるデータ構造を組み合せて表現されて

いる.consセルは図 A.1左に示すように,car部と cdr部とよばれる 2つの欄 (ポイン

タ値が入る)から成っている.以下では見やすさのためこれを図A.1右のようにかぎ形

で書き表す.リスト構造は consセルを例えば図 A.2のように組み合せて構成されてい

る.たいていの場合はリストの末尾は終りの印 nilになっているが,もしそうでない

場合には図 A.2中のように「.」で区切った後に末尾の要素が表示される.

リスト構造を組み立てるのには関数「(cons X Y )」を用いる.consは car部がX,

cdr部が Y であるような consセルを新たに作り出して返す.したがって図 A.2上の構

造は次のように consのみを組み合せて組み立てることができる.

>(cons ’a (cons ’b (cons ’c nil)))

(A B C)

Page 290: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

290 付録 A LISPプログラムの読み方

しかしこれでは煩わしいことも多いので,「(list X1 . . .Xn)」によって X1 . . .Xn

を要素とするリストを作り出すこともできる.

逆にリストの構成要素を取り出すのには,car部を取り出す関数 carと cdr部を取り

出す関数 cdrを用いる.例えば次のような具合である.

>(car ’((a b) c d)

(A B)

>(cdr ’((a b) c d)

(C D)

>(car (cdr ’((a b) c d)))

C

>(car (cdr (cdr ’((a b) c d))))

D

carと cdrは連続して組み合せて使用することが多いので,下 2 つであれば cadr,

caddrのように (最大 4個まで)組み合せた関数があらかじめ用意されている.

バッククォート式

「いつもある決まった形の S式」を指定したい場合には先に述べた「’」を使えばよい.

しかし,場合によっては「ほとんど決まった形だが,一部が計算の結果求まる」よう

なものを扱いたい場合もある.そのようなときには「’」の代りに「‘」を使い,計算

したい部分の前に「,」を前置する.

> ‘(a b ,(+ 1 2) d)

(A B 3 D)

なお,リストの「特定の場所」に計算結果を埋め込みたい場合にはこのように「,」

を使うが,「リストのここから後ろ」に埋め込みたい場合には次のように「,@」を使う.

> ‘(a b ,@(cons 1 2))

(A B 1 . 2)

関 数 定 義

関数は,特殊形式 defunにより定義する.その基本形は次の通り.

Page 291: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 A LISPプログラムの読み方 291

(defun 名前 ([引数名…] [&rest リスト引数名] [&aux 局所変数]) 式…)

ここで []で囲んだ部分はそれぞれあってもなくてもよい.「名前」は定義する関数の

名前を表す.「引数名…」は通常引数を表す.関数呼出し時に n個の引数を書くと,そ

れらは 1番目,2番目,…,n番目の通常引数の値として束縛される.関数の実行はそ

の状態で「式…」を順に実行することによって行われる.例えば次に 2つの数値 x,y

を受け取って 2x+ yを計算する関数の例を示す.

>(defun func1 (x y) (+ (* 2 x) y))

FUNC1

>(func1 2 3)

7

関数の中で作業用に局所変数を使用する場合には,&auxに続けてその名前を列挙す

る.名前の代わりに「(名前 式)」という形のものを書くと,「式」によって与えられ

た初期値をもたせることができる.引数や局所変数に値を代入することは「(setq 変

数名 式)」を評価することによって行える (なお,引数でも局所変数でもない記号に

値を代入すると,それは広域変数であるものと見なされる).例えばこの機能を用いて

4x+ 2y + zを計算する関数は次の通り.

>(defun func2 (x y z &aux (s x))

(setq s (* 2 s)) (setq s (+ s y))

(setq s (* 2 s)) (setq s (+ s z))

s)

FUNC2

>(func2 3 2 1)

17

このように,本体の「式」が複数ある場合はそれは順番に評価され,最後の式の値 (こ

の場合は「s」)が全体の値となる.もう 1つの場合として,関数内部で「(return-from

関数名 式)」が評価されるとただちにこの「式」の値が関数の値として返され関数の

実行は修了する.関数によっては,可変個の引数を受け取りたい場合がある.そのよう

なときは通常引数 (0個でもよい)に続けて「&rest リスト引数名」を指定しておくと,

通常引数に対応しなかった引数は全てリストとしてこのリスト引数に対応させられる.

これを利用して 2nx1 + 2n−1x2 + . . .+ 2xn−1 + xn を計算する関数を示す (dolistに

ついては後述).

Page 292: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

292 付録 A LISPプログラムの読み方

>(defun func3 (&rest l &aux (s 0))

(dolist (x l) (setq s (+ s s x)))

s)

FUNC3

>(func3 3 2 1)

17

制御構造 — 選択

Lispで最も重要な制御構造は (特に再帰的な)関数呼出しと返値だが,もちろん条件分

岐のようなものも必要である.条件分岐には ifと condの 2つの特殊形式がある.if

は次の形をとる.

(if 式 1 式 2 [式 3])

すなわち,「式 1」を評価して値が nilでなければ「式 2」が評価され,それが if式

全体の値となる.nilであれば「式 3」が評価されそれ (式 3が省略されている場合に

は nil)が if式全体の値となる.condは次の形をしている.

(cond (式 1 式…)

(式 2 式…)

…(式 n 式…))

すなわち,まず式 1を評価し,それが nilなら式 2を評価し,…のようにして最初

に nilでない値を返した「枝」が選ばれる (最後の枝まで nilだったら,全体の値も

nilとなる).枝が選べたら,それに引き続く「式…」を順次評価して行き,最後の式

の値が cond全体の値となる.これらに加え case式も用意されている.その形は次の

通り.

(case 式 1

((リスト 1) 式…)

((リスト 2) 式…)

…)

これはまず式 1を評価し,その値がリスト 1の要素どれかと等しければ枝 1,そうで

なくてリスト 2の要素のどれかと等しければ枝 2,のようにしていずれかの枝を選び,

枝が選べたらその後に続く各式を順次評価して最後の値を case式全体の値とする.

Page 293: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 A LISPプログラムの読み方 293

制御構造 — 反復

反復は原理的には常に再帰によって代替できるが,すなおに反復の制御構造を用いる

方が自然な場合も多い.ループはどれも式 (return) を実行することで抜け出すこと

ができる.まず

(loop 式…)

は無限ループを表す.抜け出すには常に return,return-fromなどを使う必要があ

る.もう 1つのよく使う形式は

(dotimes (変数 回数) 式…)

である.これは「変数」を 0,1,2,…と変化させながら指定した回数だけループを繰

り返す.あと,

(dolist (変数 リスト) 式…)

もよく使われる.これはリストの各要素を順番に「変数」に入れながらループするも

のである.最後に,最も一般的なループ構文 doは次の形をしている.

(do ((変数 1 初期値 1 更新式 1) (変数 2 初期値 2 更新式 2) …)

(終了条件 終了値) 式…)

すなわち,ループ変数として変数 1,変数 2,…の任意個が使用でき,それぞれにつ

いて初期値とループ 1周回ごとの更新のための式が指定できる.そしてループが終わ

る条件と終ったときの値を定める式 (なければ nilとなる)を指定し,その後で繰り返

し評価される本体を記す.

マップ関数と関数引数とラムダ式

マップ関数とは,「リストの各要素に対して…を施す」ような種類の関数である.その

一番代表的なものは mapcarである.

>(defun add2 (x) (+ x 2))

ADD2

>(mapcar #’add2 ’(1 2 3 4))

(3 4 5 6)

Page 294: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

294 付録 A LISPプログラムの読み方

なお,「#’」は「以下に続くのは関数である」ということを表す記法である.ところ

で,上では「2つ増やす」という関数を名前をつけて定義していたが,そうする代りに

ラムダ式という形式で関数定義を直接書くこともできる.

>(mapcar #’(lambda (x) (+ x 2)) ’(1 2 3 4))

(3 4 5 6)

ラムダ式の記法は defunの「defun 名前」が「lambda」が変っただけである.

属 性 リ ス ト

ここまでに,値を保存しておく場所として「引数」「局所変数」「広域変数」(いずれも

形としては記号)が出てきた.これらに加えて,各記号には「属性リスト」とよばれる

記憶領域が付随している.属性リストに入っている値の参照は

(get 記号 属性名)

による.「属性名」もまた記号である.値を設定するには

(setf (get 記号 属性名) 値)

による.例えば次の例を参照されたい.

>(setf (get ’x ’kind) ’banana)

BANANA

>(get ’x ’kind)

BANANA

なお,setfは一般に各種の「場所」に値を設定するために使うことができる.例え

ば変数に対して setfを行うと,それは setqと同じ効果をもつ.

連 想 リ ス ト

連想リストとは「((キー …) (キー …) …)」のような形をしたリストであり,関数

assocによって指定したキーをもつ部分リストを検索することができる.

Page 295: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 A LISPプログラムの読み方 295

>(assoc ’a ’((b 1 2) (c 2 4) (a 1 3) (d 2 4)))

(A 1 3)

連想リストは小規模な表ないしデータベースとして使うのに適している.

pushと pop

Lispでは consを使ってリストの先頭に値を追加したり,carと cdrを使って先頭要素

と残りに分けたりするのが効率良く行えるので,これらの操作によってリストをスタッ

クのように使うことも多い.それをわかりやすく書くため次のものが用意されている.

>(setq s ’(a b c))

(A B C)

>(push ’x s) ← sの先頭に’xを追加(X A B C)

>(push ’y s) ← sの先頭に’yを追加(Y X A B C)

>(pop s) ← sの先頭要素を取り除いて返すY

>s ← sからは確かに Yが取り除かれた(X A B C)

マ  ク  ロ

自前で制御構造のようなものをつくろうと思ったとき,関数定義ではうまくいかない

ことが多い.というのは,関数では引数は必ず評価されてしまうのに対し,制御構造

では (例えば ifの式 2や式 3を考えればわかるように)何か条件が成り立ったら始め

て式を評価するようにしたいのが普通だからである.そのようなときには,マクロが

使える.マクロは defunの代りに defmacroで定義する.そして,マクロの実行はま

ずマクロを関数のように呼び出して,マクロ内部で「実行したい式」を組み立てて返

し,その返された式を改めて評価する,という 2 段階で行われる.例えば次の例を見

てみる.

>(defmacro do10times (x) ‘(dotimes (i 10) ,x))

DO10TIMES

>(setq a 1)

Page 296: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

296 付録 A LISPプログラムの読み方

1

>(do10times (setq a (+ a 1))) ←★NIL

>a

11

すなわち,★のところでマクロが呼ばれるが,それは結果として「(dotimeis (i

10) (setq a (+ a 1)))」を返す.そして,それが改めて評価されるので,aの値が

10回増やされることになる.

副  作  用

setfは実は consセルの car部や cdr部を書き換える機能ももつ.

>(setq x ’(a))

(A)

>(setf (cdr x) 1)

1

>x

(A . 1)

それ自体を直接使うことはないが,2つのリストの連結を行う関数「(nconc L1 L2)」

はリスト L1 の末尾を書き換えることによって連結を実現するので注意が必要である

(appendは副作用を避けるため L1を複写する).

>(setq x ’(a b))

(A B)

>(append x ’(c d))

(A B C D)

>(nconc x ’(e f))

(A B E F)

>x

(A B E F)

入  出  力

関数 readは標準入力から S式を読み込んでそれを値として返す.

Page 297: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 A LISPプログラムの読み方 297

>(read)

(a b c d) ←★(A B C D) ←☆

なお,★はキーボードからの入力で☆は readの値である.また,printはその引数

を標準出力に書き出す.prin1は同様だが,ただし出力に先だって改行しない.

>(dotimes (i 3) (print x) (prin1 x))

(A B C D)(A B C D)

(A B C D)(A B C D)

(A B C D)(A B C D)

より書式を整えた出力のためには formatを用いる.「(format t "書式文字列" 式

…)」により,書式文字列中の「~A」となっている箇所に式が 1個ずつ埋め込まれた形

で出力が組み立てられる.また,書式文字列中に~%があるとそこで改行が起きる.

>(format t "x = ~A~%car[x] = ~A" x (car x))

x = (A B C D)

car[x] = A

NIL

なお,2番目の引数を tでなく nilとすると標準出力に出力される代りに同じ内容

が文字列として組み立てられて返される.

関数と特殊形式の一覧

(and C1 C2 …) : 条件C1,C2,…を順に評価し,全てが tであれば値として tを

返す.nilが 1つでもあれば,そこから先は評価せず値として nilを返す.

(append L1 L2) : リスト L1と L2を連結したリストを値として返す.

(apply f L) : 関数 f を引数のリスト Lに適用し,その結果を値として返す.

(assoc K alist) : alistの中からキーK をもつ項目を探して返す.

(atom X) : X がアトムであれば t,そうでなければ nilを返す.

(car L) : リスト Lの先頭要素を返す.

(case X 枝 …) : X の値によって分岐する.

(catch タグ E) : 式 E を評価しその値を返す.ただしその中で同じタグを指定し

た throwが実行されたときはそれを受けとめる.

Page 298: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

298 付録 A LISPプログラムの読み方

(cdr L) : リスト Lの先頭要素を除いた残りを返す.

(cond (枝 …)) : 条件による場合分け.

(cons X Y ) : X と Y を要素とする cons対をつくる.

(decf N) : N の値を 1減らす.

(defmacro 名前 引数 本体) : マクロを定義する.

(defun 名前 引数 本体) : 関数を定義する.

(do (変数指定) (終了判定) 本体) : 複数の変数を更新しながらループする.

(dolist (V L) 式…) : リスト Lの各要素を変数 V に束縛しながらループする.

(dotimes (V N) 式…) : 変数 V を 0からN − 1まで変化させながらループする.

(eq X Y ) : X と Y が同じ記号やコンス対かどうかを調べる.

(eql X Y ) : X と Y が同じ値かどうか (eqの場合も含む)を調べる.

(equal X Y ) : X と Y が同じ構造をもつかどうか (eqlの場合も含む)を調べる.

(eval E) : E を 1回余計に評価する.

(expt N M) : N のM 乗を返す.

(format 行き先 書式文字列 値…) : 書式つき出力.

(get 記号 属性) : 記号の属性値を返す.

(if 条件 枝 1 枝 2) : 条件によって枝 1か枝 2を評価する.

(incf N) : N の値を 1増やす

(intern S) : 文字列 S と同じ名前をもつ記号を (なければつくり)返す.

(lambda (引数部) 本体) : ラムダ形式.

(list X1 … Xn) : X1…Xn)を要素とするリストを返す.

(loop 本体) : 本体を反復実行する.

(mapcar F L) : リストLの各要素に関数F を適用し,結果の値をリストとしてま

とめたものを返す.

(maplist F L) : L,(cdr L),(cddr L),…に関数 F を適用し,結果の値をリ

ストとしてまとめたものを返す.

(member X L) : リスト Lの中に X と eqlなものがあればその部分以降を返し,

なければ nilを返す.

(nconc L1 L2) : リスト L1の末尾を書き換えて L2を後につなげる.

(not C) : C が nilなら t,そうでなければ nilを返す.

(nth N L) : Lの N 番目の要素を返す.

Page 299: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 A LISPプログラムの読み方 299

(null X) : X が nilなら t,そうでなければ nilを返す.

(numberp X) : X が数値なら t,そうでなければ nilを返す.

(or C1 C2 …) :条件C1,C2,…を順に評価し,全て nilであれば nilを返す.nil

以外が 1つでもあればその先は評価せず,その nil以外だった値を返す.

(pop V ) : 場所 V に入っているリストの先頭要素を返し,S の内容は先頭要素を

除いた残りに書き換える.

(print X) : X を標準出力に書き出す.

(prin1 X) : X を改行なしで標準出力に書き出す.

(progn 式 1 式 2 …) : 式 1,式 2,…を順に評価し,最後の式の値を全体の値と

して返す.

(push X V ) : 場所 V に入っているリストの先頭にX をつけ加えたものを新しい

V の値とする.

(quote X) : X を評価しないまま値とする.

(read) : 標準入力から S式を読み込み値として返す.

(remove X L) : リスト LからX と eqlなものを取り除いたリストを返す.

(remove-if F L) : リスト Lから F を適用したとき nilでない値を返すような要

素を取り除いたリストを返す.

(return E) : 関数や最内側のループから値E をもって抜け出す.

(return-from 関数名 E) : 指定した関数から値 E をもって抜け出す.

(reverse L) : リスト Lを逆順にしたリストを返す.

(set X E) : X を評価した結果の記号の値としてE を格納する.

(set-difference L1 L2) : L1からL2に含まれる要素を全て除いたリストを返す.

(setf P E) : 場所 P の値として E を格納する.

(setq V E) : 変数 V の値として E を格納する.

(sort L F) : リスト Lを関数 F が定める大小関係に従って昇順に並べる.

(string-downcase S) : 文字列 S 中の大文字を小文字に取り替えた文字列を返す.

(symbol-function V ) : 記号 V に対応する関数を返す.

(symbol-name V ) : 記号 V の名前文字列を返す.

(symbol-value V ) : 記号 V に格納されている値を返す.

(symbolp X) : X が記号であれば t,そうでなければ nilを返す.

(throw タグ E) : 指定したのと同じタグをもつ catchまで戻り,E をその catch

Page 300: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

300 付録 A LISPプログラムの読み方

の値とする.

(trace 関数名のリスト) : 指定した関数をトレースモードにする.

(union L1 L2) : L1に含まれている記号と L1に含まれていなくて L2に含まれて

いる記号とを合せたリストを返す.

(zerop N) : N がゼロであれば t,そうでなければ nilを返す.

Page 301: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

301

付 録B 参 考 文 献

言語処理系というのは非常に広い範囲をカバーする話題であり,1冊の本に収められる

話題はごく限られている.また,「はじめに」で記したように,本書では話題の網羅性

よりは具体的なイメージを与えることに力点を置いているので,より詳しい事柄につ

いて知りたい読者は以下にあげるものを含めて参考文献を積極的に参照して頂きたい.

上記のような話題の性質上参考文献自体も非常に多数存在するので,ここでは単行本

(できれば和書)として入手しやすく読みやすいものを優先してあげるようにしている.

興味のあるテーマについて,これらの本や論文にあがっている参考文献をまた引きす

ることもお勧めしたい事柄である.

B.1 コンパイラの教科書

コンパイラの教科書 (本書もその 1つではあるが)は多数出版されていて,それぞれに

特徴がある.内容によっては直接論文や雑誌記事に当たるよりこれらの教科書を読む

方がわかりやすいものも多い.

[1] 佐々政考: プログラミング言語処理系, 岩波書店, 1989.

[2] 疋田輝雄, 石畑 清: コンパイラの理論と実現, 共立出版, 1988.

[3] Aho, A. V., Sethi, R., Ullman, J. D.: Compilers — Principles, Techniquies,

and Tools, Addison-Wesley, 1986. (邦訳) 原田賢一訳: コンパイラ — 原理・技

法・ツール I/II, サイエンス社, 1990.

[4] Pittman, T., Peters, J.: The Art of Compiler Design, Prentice-Hall, 1992.

[5] Fischer, C. N., LeBlanc, Jr., R. J.: Crafting a Compiler, Benjamin/Cummings,

1988.

Page 302: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

302 付録 B 参 考 文 献

[1]は 600ページにもなる大著で,特に本書ではあまり詳しく取り上げなかった属性文

法について詳しく書かれている.また参考文献が解説つきで丁寧にあげられていて利

用価値が高い.[2] はコンパクトな本でありながら,必要な事項についてきっちりとま

とめられている.さらに簡略化されたC言語の処理系が掲載されていて,打ち込んで

動かすことができる.[3]はコンパイラの教科書の「定版」とされていて,各種の事項

について詳しく網羅的に解説されている.[4]は属性文法を全面的に採用したもので,

意味解析,各種の最適化,コード生成を属性変換文法に従って記述してある.属性文法

による実用的な処理系記述に興味のある読者にはお勧めである.[5]もわかりやすく読

みやすい本であり,想定している言語がAdaなので記号表,モジュール,オーバロー

ディングなどについて丁寧に書かれている.その他,エラー回復などについても詳し

く解説されている.

B.2 ほんもののコンパイラ

コンパイラを勉強するにはほんもののコンパイラを読むのがよい,とは昔から言われ

ていることである.そして,幸いなことに現在では「おもちゃでない」コンパイラの

ソースコードを入手して読むこともそう難しくはない.

[6] Writh, N.: Algorithm + Data Structures = Programs, Prentice-Hall, 1976. (邦

訳) 片山卓也訳: アルゴリズム+ データ構造=プログラム,日本コンピュータ協会,

1979.

[7] Pemberton, S., Daniels, M. C.: Pascal Implementation, The P4 Compiler, Ellis

Horwood, 1982. (邦訳) 武市正人, 木村友則訳: Pascalの言語処理系, 近代科学社,

1984.

[8] Richards, M., Whitby-Strevens, C.: BCPL — The Language and its Compiler,

Cambridge University Press, 1979. (邦訳) 和田英一訳: BCPL — 言語とそのコ

ンパイラ, 共立出版, 1985.

[9] GNU C Compiler, version 2.3.3, Free Software Foundation, 1993.

[10] Richard M. Stallman: Using and Porting GNU CC, preliminary draft for

version 2.3, Free Software Foundation, 1992.

Page 303: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 B 参 考 文 献 303

[11] フリーソフトウェアファンデーション編著, 岩谷 宏, 都田克郎訳: インサイド

GNU C コンパイラ, 啓学出版, 1991.

[6]には小規模な言語 PL/0のコンパイラを Pascalで記述したものが含まれている.フ

ルセットのPascal言語がよければ [7]は広く流布している Pascal-P4コンパイラのソー

スコードと解説書である.また [8]には C言語の祖先に当たる BCPLの処理系が掲載

されている.これらが古典的なコンパイラでコードも素直な仮想スタックマシンコー

ドなのに対し,[9]は本格的な最適化を行うリターゲット可能な実用の翻訳系のソース

コードである.これは開発元であるFree Software Foundationから,およびネットワー

ク経由ほか多くのセカンドソースから入手可能である.動かして見られる環境がある

のなら練習問題にあげたように RTLのダンプを眺めるだけでも勉強になるし,自分で

目的機械記述を書いてみるのも面白いかもしれない.そして,ソースコードと一緒に

ドキュメントとして [10]が配布される.これを出力するには文書生成系 TeXが必要で

ある.バージョン 1.26のものでよければ邦訳 [11]が出版されている.

B.3 マクロプロセッサ・プリプロセッサ・ツール

言語処理系として実用的であるためには,コンパイラだけでなく周辺の各種ツールとの

連携も重要である.またコンパイラを構成する上で構文解析器生成系などの各種ツー

ルについて知っておくことも必要である.

[12] Brown: Macro Processors and Techniques for Portable Software, Wiley, 1974.

(邦訳) 鳥居宏次, 杉藤芳雄, 真野芳久訳: マクロ・プロセッサとソフトウェアの移

植性, 近代科学社, 1977.

[13] Kernighan, Plauger: Software Tools, Addison-Wesley, 1976. (邦訳) 木村 泉

訳: ソフトウェア作法, 共立出版, 1981.

[14] Schreiner, A. T., Friedman, Jr., H. G.: Introduction to Compiler Construction

with Unix, Prentice-Hall, 1985. (邦訳) 矢吹道郎, 小暮博道, 田中啓介訳: Cコン

パイラ設計 — yacc/lexの応用, 啓学出版, 1987.

マクロプロセッサは本書ではごく簡単にしか扱わなかったが,多くの興味深い側面を

もている.[12]はマクロプロセッサの諸側面,およびその技術を適用したソフトウェア

Page 304: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

304 付録 B 参 考 文 献

の移植性について触れられており,とても面白い本である.また,[13]にもマクロ処

理系とその様々な特性についての記述がある.さらにRatforとよばれる Fortranプリ

プロセッサについて説明がある.いずれもプログラムが掲載されていてその気になれ

ば動かしてみることもできる.Unixのツール lexと yaccについては雑誌の各種記事を

はじめ多数あるが,たとえば [14]が実用的である.

B.4 C言語とLisp言語

本書で例の記述に用いた C言語と Lisp言語については多くの本が出ているが,最小限

あげておく.

[15] Kernighan, B. W. K., Ritchie, D. M.: The C Programming Language, Prentice-

Hall, 1978. (邦訳) 石田晴久: プログラミング言語 C, 共立出版, 1981.

[16] Steele, Jr. G. L.: Common Lisp: The Language 2nd Edition, Digital Press,

1990. (邦訳)井田昌之 翻訳監修: bit別冊 Common Lisp第 2版, 共立出版, 1991.

[17] Anderson, J. R., Corbet, A. T., Reiser, B. J.: Essential Lisp, Addison-Wesley,

1987. (邦訳) 玉井 浩: これが Lispだ, サイエンス社, 1989.

[15]は第 2版が出ているがこちらは ANSI C準拠で本書の例と書き方がやや違うので

旧版を挙げた.[16]はCommon Lispの原典なのでここにあげたが,とても厚い本なの

で,ざっと読んで Lispの勉強をしようと思うなら例えば [17]などの方がよい.

B.5 その他の単行本

本書では理論的枠組については最低限しか触れなかったが,これらについても分野ご

とに各種の本が出ている.

[18] Hopcroft, J. E., Ullman, J. D.: Introduction to Automata Theory, Languages,

and Computation, Addison-Wesley, 1979. (邦訳)野崎昭弘ほか: オートマトン 言

語理論 計算論 I/II, サイエンス社, 1984/1986.

[19] 徳田雄洋: 構文解析, 昭晃堂, 1989.

Page 305: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

付録 B 参 考 文 献 305

[20] Cleaveland, J. C.: An Introduction to Data Types, Addison-Wesley, 1986.(邦

訳) 小林光夫: データ型序説, 共立出版, 1990.

B.6 各種論文・解説

「序文」でも述べたように,比較的新しいアイデアについては,一部教科書で触れら

れているものもあるが,基本的には論文などを当たらなければならない.これは非常

に多数あるので,ここまでに記した単行本でカバーされないものを中心にテーマごと

に一例を示しておく.

[21] Catanzaro, B. J. ed.: The SPARC Technical Papers, Springer-Verlag, 1991.

[22] 特集: 新しいアーキテクチャに基づくコンパイラ技術,情報処理学会誌 1990年

6月号, 1990.

[23] Special Section on Supercomputing, CACM, vol. 35, no. 8, 1992.

[24] Aho, A. V., Ganapathi, M., Tjiang, S.: Code Generation Using Tree Matching

and Dynamic Programming, ACM Transactions on Programming Languages

and Systems, vol. 11, no. 4, pp.491-516, 1989.

[25] Christopher, T. W., Hatcher, P. J., Kukuk, R. C.: Using Dynamic Program-

ming to Generate Optimized Code in a Graham-Granville Style Code Generator,

Proc. SIGPLAN ’84 Symposium on Compiler Construction, pp. 25-36, 1984.

[26] Davidson, J. W., Fraser, C. W.: Code Selection throught Object Code Opti-

mization, ACM Transactions on Programming Languages and Systems, vol. 6,

no. 4, pp.505-526, 1984.

[27] Morris, W. G.: CCG: A Prototype Coagulating Code Generator, Proc. SIG-

PLAN ’91 Conference on Programming Language Design and Implementation,

pp. 45-58, 1991.

[28] Himelstein, M. I., Chow, F. C., Enderby, K.: Cross-Module Optimizations:

Its Implementation and Benefits, Proc. Summer 1987 USENIX Conference,

pp.344-356, 1987.

Page 306: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

306 付録 B 参 考 文 献

[29] Chow, F., Hennessy, J. L.: The Priority-Based Coloring Approach to Register

Allocation, ACM Transactions on Programming Languages and Systems, vol.

12, no. 4, pp. 501-536, 1990.

[30] Gibbons, P. B., Muchnick, S. S.: Efficient Instruction Scheduling for a Pipelined

Architecture, Proc. SIGPLAN ’86 Symposium on Compiler Construction, pp.

11-16, 1986.

[31] Wall, D. W.: Predicting Program Behavior Using Real or Estimated Pro-

files, Proc. SIGPLAN ’91 Conference on Programming Language Design and

Implementation, pp. 59-70, 1991.

[32] Boehm, H-. J., Demers, A. J., Sshenker, S.: Mostly Parallel Garbage Collec-

tion, Proc. SIGPLAN ’91 Conference on Programming Language Design and

Implementation, pp. 157-164, 1991.

[33] Granlund, T., Kenner, R.: Eliminating Branches Using a Superoptimizer

and the GNU C Compiler, Proc. SIGPLAN ’92 Conference on Programming

Language Design and Implementation, pp. 341-352, 1992.

[34] Maydan, D. E., Hennessy, J. L., Lam, M. S., Efficient and Exact Data De-

pendence Analysis, Proc. SIGPLAN ’91 Conference on Programming Language

Design and Implementation, pp. 1-14, 1991.

[35] Cooper, K., Kennedy, K.: Fast Interprocedural Alias Analysis, Proc. 16th

ACM Symposium on Principles of Programming Languages, pp. 49-59, 1989.

[21]には SPARC用コンパイラについての解説が含まれている.[22]には様々なアーキ

テクチャ依存のコンパイラ技術についての解説記事が載っている.[23]にはベクトル/並

列計算機のためのコード生成に関する解説記事が含まれている.[24]は木のマッチング

によるコード生成器,[25]はGraham-Granvilleコード生成器,[26]はDavidson-Fraser

コード生成器,[27]は凝固型コード生成器に関するものである.[28]は手続き間最適化

の実例をあげている.[29]はレジスタ彩色,[30]は命令スケジューリング,[31]はプロ

ファイルの利用,[32]はごみ集め,[33]はスーパーオプティマイザ,[34]はデータ依存

解析,[35]は手続き間解析に関するものである.

Page 307: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

307

索 引

1パスコンパイラ, 106

BNF, 34

DAG, 181

Davidson-Fraserコード生成器, 277

DFA, 38

DU-連鎖, 229

First, 59

First, 58

Follow, 80

Follow, 58

Follow, 59

goto, 202

Graham-Granvilleコード生成器, 276

LALR(1)解析器, 87

lex, 48, 99

LL(1)解析器, 63

LL(1)かいせきき, 64

LL(1)文法, 66

LR(1)項, 85

LRオートマトン, 72

LR解析器, 72

NFA, 38

RISCアーキテクチャ, 281

RTL, 277

SLR(1)解析器, 79

UD-連鎖, 228

yacc, 97, 116

曖昧でない定義, 229

あいまいな文法, 93

曖昧な文法, 93

あいまいな文法, 35

アセンブラ, 14, 24

アセンブリ言語, 14

値呼び, 159, 206

あと埋め, 135

後始末コード, 205

誤り回復, 95

誤りからの回復, 95

誤り修復, 96

アルファベット, 29

行き先表, 83

生きている変数, 225

Page 308: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

308 索 引

依存解析, 285

意味, 16

意味解析, 18

意味解析, 105

意味規則, 109

意味スタック, 114

入口, 206

インタプリタ, 17, 24, 179

インタリーブ, 21

上向き解析, 58, 70, 114

後ろ向きフロー解析, 228

右辺値, 162

上向き解析器, 114

永続ループ変数, 245

エラー記号, 96, 101

演算強さの軽減, 232, 246

オートマトン, 33

解釈実行型コード生成, 260

解釈実行系, 17

解析器, 32

解析スタック, 63, 70, 114

解析表の圧縮, 97

解析部, 22

仮想機械語, 25

型, 105, 141

型の同一性, 142

型の同定, 147

型表, 146

活性レコード, 156

環境の切り替え, 165

還元, 79

還元- 還元衝突, 80

キー, 124

記憶領域, 154

機械語, 14, 180

記号実行, 231

記号表, 20, 123

記述主導型コード生成, 270

帰納変数, 245

木の書き換え, 270

帰辺, 220

基本型, 187

基本機能変数, 246

基本ブロック, 216

境界揃え, 155

共通式, 209, 230, 233, 238

共通部分式, 181

局所最適化, 217

行順序, 191

くし刺しコード, 183

組単位の展開, 255

ケース分析, 255

計算機言語, 14

計算規則, 107

形式言語, 29

決定性有限オートマトン, 38

検索, 124

言語処理系, 13, 14

コード生成, 20

コード生成器生成系, 270

項, 72

広域 goto, 202

Page 309: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

索 引 309

広域最適化, 217

広域レジスタ割り付け, 262

高水準言語, 15

構造型, 187

構造による同一性, 143

後置コード, 182

構文, 16

構文解析, 18, 57

構文解析器生成系, 97

構文解析表, 83

構文木, 34, 57, 106, 107, 116

構文指示翻訳, 109, 111

コピー伝搬, 233, 240

コピー文, 230

コンパイラ, 17

コンパイラ・インタプリタ, 17

コンパイル& ゴー, 22

コンパイル環境情報, 138

語, 29

合成属性, 107

ごみ集め, 172

最右導出, 34

再帰下降解析器, 67, 111

再帰下降解析器, 111

再帰上昇解析器, 90

最左導出, 34

最終状態, 33

再定義, 136

最適化, 215

再配置可能コード, 23

先読み記号, 63, 70, 85

左辺値, 162

サンク, 162

参照計数型ごみ集め, 175

参照呼び, 160, 207

算術式, 193

3番地コード, 184

自然ループ, 220

下向き解析, 58

下向き解析, 111

下向き解析器, 111

支配する節, 220

シフト-還元解析器, 70

シフト-還元衝突, 93

シフト-還元衝突, 80, 99

修飾された, 107

修飾された構文木, 185

終端記号, 30

出発記号, 30

衝突, 127

初期状態, 33

死んでいる変数, 226

字句解析, 18, 37

実行系, 17

実行時ライブラリ, 24

自動コールライブラリ, 24

自動的な型変換, 147

順位, 94

状態, 33

数学的等価, 232

スコープ規則, 130, 136

スタック, 154, 156, 182

Page 310: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

310 索 引

スタックの巻き戻し, 204

スタックフレーム, 156

スタックポインタ, 157

スピル, 266

正規言語, 31

正規表現, 31

正規表現, 35, 38, 48

正規文法, 31

正規文法, 35

制御構造, 105

制御フロー解析, 220

正準 LR(1)解析器, 87

正準 LR(1)解析器, 83

生成規則, 30

生成部, 22

生成文法, 30

生成文法のクラス, 31

静的束縛, 153

静的チェイン, 165

接合器, 278

遷移, 33

線形探索, 124

漸進型ごみ集め, 175

前置記法, 121, 276

前方参照, 106, 135

ソースコード, 17

相続属性, 107

束縛, 153

その場展開, 251

属性, 107

属性文法, 107

退避回復, 164

タグ, 173

多重定義, 147, 148

単一化, 143

単一代入, 109

端記号, 30

単導出, 30

段, 20

断片化, 171

チェインリハッシュ, 127

遅延分岐, 281

遅延ロード, 281

中間コード, 22, 179, 180

注釈, 52

抽象構文木, 119, 180

中置記法, 121

つづり, 18, 37

詰合, 174

低水準言語, 14, 15

定数の畳み込み, 232

低レベル中間木, 270

手続き間最適化, 216, 250

手続き間レジスタ割付け, 253

手続き内最適化, 216

手続き引数, 168

テンポラリ, 184, 185

データフロー方程式, 226

データフロー解析, 224

ディスプレイ, 167

出口, 206

トークン, 18, 37

Page 311: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

索 引 311

到達する定義, 229

飛越し型コード, 194

動作表, 83

導出, 30

動的束縛, 154

動的チェイン, 166

独立翻訳, 137

名前による同一性, 142

名前の束縛, 153

名前の同定, 105

名前呼び, 161, 207

2分木, 125

2分探索, 124

認識器, 32

のぞき穴最適化, 268

配列, 190

配列の線形化, 249

ハッシュ表, 126

範囲解析, 231

バックエンド, 22

パイプライン, 280

パス, 21

パターンマッチ, 270

パニックモード, 95

ヒープ, 154, 169

比較演算, 193

引数, 159

非決定性有限オートマトン, 38

非終端記号, 30

非循環有向グラフ, 181

非端記号, 30

左結合的, 94

左再帰性, 66

表, 124

フェーズ, 20

複写型ごみ集め, 174

複写復元呼び, 162, 207

不到達コードの削除, 233

不要コードの削除, 233, 240

フレームポインタ, 158

フローグラフ, 217

フロントエンド, 22

ぶらさがり else, 93

ブロック型スコープ, 130

文, 29

分割翻訳, 137

文脈依存文法, 31, 105

文脈自由文法, 31, 57

プリプロセッサ, 22

プリヘッダ, 223

プログラミング言語, 14

併合, 87

閉包, 45, 75

ヘッダ, 220

変換系, 17

返却, 170

変数への定義, 229

返値, 159, 163, 208

ベクトル化, 284

ベクトルプロセッサ, 284

ホイスティング, 234

翻訳系, 17

Page 312: 言 語 プ ロ セ ッ サ - University of Electro ...ka002689/sysof17/lp.pdf · 結果,より進んだ最適化,多数のレジスタの割当て,ハードウェアに密着した命令ス

312 索 引

マーカ, 115

マークスイープ型ごみ集め, 173

前向きフロー解析, 229

マクロ, 251

マクロ機構, 162

マクロプロセッサ, 22

マルチプロセッサ, 284

右結合的, 94

3つ組, 184

無結合的, 94

命令スケジューリング, 280

命令セットアーキテクチャ, 14

メモリの漏洩, 171

目的コード, 17, 179, 255

モジュール, 133

モジュールデータベース, 138

文字列領域, 128

有限オートマトン, 33, 38

ユニオン, 192

4つ組, 184

呼ばれ側での保存, 164

呼び側での保存, 164

呼出しグラフ, 251

予約語, 52

ライブラリ, 188, 193

ラベル, 202

ランダムリハッシュ, 127

領域回復, 170

利用可能式, 230

リンカ, 23

ループ最適化, 242

ループ展開, 248

ループ内分岐の移動, 245

ループの同定, 220

ループ不変文, 243

ループ融合, 249

例外処理, 203

例外ハンドラ, 203

例外表, 204

レコード, 192

レジスタ干渉グラフ, 263

レジスタ彩色, 262

レジスタ転送言語, 277

レジスタ窓, 282

レジスタ渡し, 163, 207

レジスタ割当て, 262

レジスタ割り付け, 262

列順序, 191

連携編集プログラム, 23

ローダ, 24

論理式, 193