言語理論とコンパイラ
第十一回:
コード生成
2005 年 7月 7日
http://www.sw.it.aoyama.ac.jp/2006/Compiler/lecture11.html
© 2006 Martin J.
Dürst 青山学院大学
目次
- これからの予定
- 宿題
- エラー処理
- 中間表現: 名前表と構文木
- 意味解析
- コード生成
これからの日程
補講日: 7月11日 (火曜日) 4限 E202
最終の授業: 7月14日 (金曜日) 2限 E202
期末試験: 7月28日 (金曜日) 2限 (11:10-12:35)
全体の日程
宿題: 複素数の電卓
提出: 再来週の金曜日 (7月7日) 10時45分、O 棟
529号室の前
簡単な電卓を複素数の電卓に拡張してください。複素数の表し方は
5i
で虚数を表し、[実部,虚部]
で複素数を表すようにして下さい。[]
内には実数演算は許されるが 5i
みたいなものが使えないように文法を設計してください。
complex.lex と complex.y を提出ください。紙の大きさは
A4。複数の紙はホチキスで止めること。
宿題の正解例
[都合により削除]
宿題のヒント
入力の一例
構文エラー処理
- エラー処理の難しさ
- エラー処理の要点
- エラーの技法
エラー処理の難しさ
- 一つの正しいプログラムに対してエラーのプログラムは多数ある
- 人間にとって間違いやすいものと間違いにくいものはプログラムに区別できない
- 構文解析には理論があるが、エラー処理には理論がない
エラー処理の要点
- 分かりやすいエラーメッセージを出す
- 一つだけではなく、複数のエラーを見つける
- 二次エラーをできるだけ出さない
- 正しいプログラムの処理を遅くさせない
- コンパイラを複雑しすぎない
エラー処理の技法
- 文法に合ったトークンを見つけるまでにトークンを捨てる
(panic mode)
- 少数のトークンを追加又は入れ替え
- 文法にエラーをキャッチする規則を追加
- 入力に一番近い正しいプログラムを探す
bison でのエラー処理
error
トークンを文法に追加できる
- エラーが起こると bison が一番近い
error
トークンの含まれる規則までエラー前のトークンや非終端記号を捨てる
- その規則の
error
トークンの後に来るトークンが来る入力も捨てる
コンパイラの段階
字句解析 (lexical analysis)
構文解析 (parsing; syntax analysis)
意味解析 (semantic analysis)
コード生成 (code generation)
最適化 (optimization)
中間表現: 名前表
(symbol table)
- 提供する機能:
- 名前の検索
- 名前の登録と取り消し
- 名前についてのデータの管理
- 要点:
- 使うことが多く、名前の数が多いので効率が大切
- 同じ名前が複数ある可能性があるので区別が必要
名前表が扱うデータ
- 名前の種類 (変数、関数、型など)
- 定義か宣言だけか
- 変数、関数などの型
- 名前が有効な領域 (例えば関数、ブロック)
- 変数などの場合: 大きさ (必要なメモリ)
- 関数、変数などの (相対) アドレス
中間表現: 構文木
(parse tree)
簡単なプログラム言語と簡単なマシーン・アーキテクチャの場合
(例えば Pascal からスタック・マシーン)
には構文解析しながらコード生成を行う
(すなわち構文木を生成しない) こともある
構文木の生成: 構文規則ごとの処理で生成。例:
expression: expression '+' term { $$ = $1 + $3; }
を次に変える:
expression: expression '+' term
{ $$ = newnode(PLUS, $1, $3; }
(YYSTYPE
も変更)
二分木が普通だが、関数の引数などに特別な措置が必要
意味解析
- 主に型の処理:
- 型が合うかどうかのチェック
- 必要に応じて型の自動変換
(構文木に新たなノードの追加)
- 構文木の生成の時に行うか後に行うか
型が同じかどうか複数の定義がある:
- 同じ名前の型が同じ (簡単だが利用者にとって不便)
- 同じ中身の型が同じ (複雑)
型の役割
プログラム言語によって型の役割が違う:
- 変数などに型がなくて、どの種類 (型)
のデータでも代入できる: Perl, Ruby, JavaScript 等
- 変数などに型を宣言しないといけない: C, C++, Java 等
- 型が必要が多くの場合にコンパイラが割り出す: Haskell
等
コード生成と最適化の関係
- 構文木で最適化、そこからコード生成
- 生成したコードを分析、最適化
- 実際は両方の組み合わせが多い
コード生成の難しさ
コード生成の手法
- 構文木を作らないで文法規則ごとにコード生成 (例:
スタック・マシーン、条件文など)
- 構文木を辿りながらノードごとにコードを書き出す
- 構文木の構造をコード生成用のパターンに比べてコードを書き出す
機械の主な種類
- スタック・マシーン:
演算は全てスタックに行われて、バーチャルマシーンに多い
- RISC:
演算は全てレジスタ内に行われて、純粋のロードとストアしかない
- CISC:
命令の数が多くて複雑 (例: Intel Pentium)
コードの書き方: アセンブリ言語
(assembly language)
- アセンブリは機械語をちゃっかん抽象化し、人間が読めるようにしたもの
- アセンブラ (assembler) によって機械語に変換される
- 機械などによって種類が多いが、書き方はだいたい似ています
- 機械命令一個を一行に書く
- 一行は四つの部分からなる:
- ラベル
- 命令 (演算子など)
- 被演算子 (レジスタ、変数名、定数など)
- コメント
超単純アセンブリ言語
- 命令は次の通り:
- LOAD: メモリ (変数) からレジスタへデータを移動
- STORE: レジスタからメモリへデータの移動
- ADD, SUB, MUL, DIV: レジスタ間の演算
- JUMP<、JUMP<= など:
二つのレジスタの比較が真でしたらラベルへジャンプ
- メモリのアドレスは変数名 (小文字) で表す
- レジスタは R1, R2,... で表し、数の制限はない
- 被演算子の順番は結果が一番最初に
(第一回の授業の宿題と違う)
if 文などのコード生成
- 条件を条件付きジャンプ命令に変更
- 条件付き命令は前の演算から残るフラグを使ったり、0
との比較が多い
- 条件が合わない場合にジャンプすることが多い
- 例:
if (a>b)
→ a-b; jumpLE0
- ジャンプの行き先がまだ分からない場合が多い
関数呼び出しのコード生成
- 呼び出し側と関数側に特別なコードが必要
- 機械・OS・言語特有の関数呼び出しスタックの構成を考慮する必要があ
- 関数呼び出しスタックの内容 (関数フレーム):
- 戻り番地 (関数後どこに戻るか)
- 引数、戻り値
- 前の関数フレームのベースポインタ
- 使われるレジスターの値を退避する一時変数
- ローカル変数