先週は、register を使った compiler の作成方法を 勉強した。最適化を考えなければ、コード生成自身はstack versionと ほとんど変わらない。これは、registerの番号の上にstackのコードを 部分計算することに相当する。先週解いた問題は、このような方法で どのようなコードが生成されるかを問うものだった。もちろん、これよりも、 賢いコード生成をおこなうことは簡単だが、それを問題の答えとするならば、 同時にコード生成のアルゴリズムを示さなければ完結した解答とはいえない。
全体構成をつかみ、Cの文法を思い出しながら、個々の関数を読んでいくことに より、実際的なcompilerの詳細を理解して欲しい。自分で、すべてを理解 しなくては、どんなプログラムでも完成させることはできない。 あてずっぽうにコードを書いても、それは絶対に動くことはないし、それは、 他のコードに影響を与え、正しい部分まで汚染してしまう。バグは避けられない ものではあるが、自分で書いたコードを完全に理解していれば、どのような バグにも対処できる。
Micor-C の全体構成複雑なプログラムになると、プログラムを一つのファイルにまとめる こともできなくなる。また、そうしなければ、複数の人間でプログラムを 書くということもできない。そこで、プログラムを分割してcompileする ということが必要になる。
分割した時に、分割した部分のどこを外に見せて、どこを隠すかという のが問題になる。この場合の単位は module と呼ばれる。 すべてを見せてしまっては、分割した意味がないとも いえる。しかし、見る方が悪いのであって、すべてはdefaultで見えている 方が良いという考え方も存在する。実際には、見せるためには、分割した もの同士の情報のやり取りが必要なので、import, export や 、public, privateなどのkeywordで情報のやり取りを制御するように することが多い。Cでは、extern と static がそれに相当する。C++では、 publicとprivateを使う。
このような情報の制御と、ファイルの分割は本来は独立な問題である。 しかし、Cではファイルの分割でしか module を定義できない。C++の 場合には class がその役割を果たしている。
Tokenizerでは、記号の切り出しとkeywordの切り出しをおこなう。同時に、 名前の登録をおこなう必要がある。実際、Tokenizerが、普通の名前、 (例えば英字で始まる英数字の列などだが)に出会ったとしよう。それは、 名前(name)、つまり、予約語(reserved word)か、変数名か関数名である。 Micor-C では、getsym() という関数がTokenizerである。
これらは compiler 内部の 表に登録されなければならない。この表を Symbol Tableという。Interpreter の場合でもSymbol Tableは必要である。Compilerが出力したコードでは、 Symbol Tableは不要である。しかし、分割compile (Separate compilation) の場合は、相互のSymbol Tableを接続する必要があるので、Symbol Table そのものも出力しなくてはならない。この問題は、異なるcomputer上で 分散計算を行う場合にも出て来る。
名前には様々な属性がある。例えば、ある名前はlocalであり、ある関数や、 関数の一部でしか有効でない。ある名前で指し示される(実体/objectゥ)ものは、名前で 呼ばれなくなった後も存在し続ける場合がある。あるいは、そのような ものを別な名前で呼びたい時もある。このような属性には以下のようなものが ある。
名前は{}の中で有効であり、その外では使えない。object も、その外では 消滅してしまうのでアクセスしてはならない。{}は入れ子(nest)することが できるので、名前の有効範囲もnestする必要がある。しかし、C では実際には、 nest は処理されずに、local 変数は、手続の中で一意な名前でなければならない。 自動的にobjectが作られ、自動的に消滅するので、 自動変数(auto)とも呼ばれる。Cではこちらの呼び方の方が正式である。 local variableのobjectは通常はstack上に取られる。したがって、 特に、pointer を使って local 変数を手続き呼び出しから返してはならない。Cでは、local変数の 初期化を行うことができる。Cにはlocalな手続き名は存在しない。
名前とobjectは、プログラム全体で有効であり、存在する。C++では、 この名前の有効範囲を class 単位にすることができる。分割 compile する場合は、この名前はコード中に出力する必要がある。分割 compile 後、さらに、link された完全なコードには symbol table は必要ない。 しかし、debug のためにわざと情報を残すこともある。(-g flag in cc)。 最近のOSでは、kernel levelで library の dynamic loadingを行う。 場合もあり、この場合もlinkのための情報を残す必要がある。 別なファイルのglobalを参照したい時には、externを使う。
objectは、プログラム全体で有効であり、存在する。しかし、名前は ファイル内部でしか有効ではない。localなstaticという名前もあり、 この場合は、名前の有効範囲は関数または{}の内部ということになる。
マクロ(macro)名は、通常compilerが処理する前に変換されてしまう。 したがって、compilerからも見えない名前である。しかし、Micor-C ではマクロも1 pathで処理するために、マクロ名も自分で管理する。 この名前の有効範囲は分割compile単位である。マクロには引数が あることがあるので、その処理も必要だが、Micro-C自身には、その 機能はない。しかし、付加することはそれほど難しくはない。
名前には、その名前が指すもの(object)の型がある。一つの名前には 一つの型がある言語(Cはそうだ)もあるが、一つの名前を複数の型に 使うことができるもの(Perlなど)もある。前者の場合は、名前を検索することに より型が分かる。しかし、後者の場合には、型は名前とは別に指定する 必要がある。後者の方がどちらかといえば優れているようである。Cには 以下のような型がある。
基本的な型。アセンブラでのword,2byte,1byteに 相当する。
符号が付くかどうか。引き算などで2の補数として 扱うかどうかに相当する。Micro-Cには、signed char, unsinged char などは 存在しない。
Micro-Cにはない。
Cを特徴づける型。直接にobjectを指すのではなく、object のaddressを表す型である。単なるaddressではなく、足し算が定義されている。 1を加えることにより、指し示すobjectの大きさの分だけaddressが加算される。 大きさは、sizeof()という内蔵関数で取り出すことができる。 sizeof(char) == 1であるけれど、sizeof(int)==sizeof(int *) であるとは保証されない、ということを覚えておこう。pointerのpointerという 型もある。これを理解しているかどうかでCを理解したかどうかが決まる重要な 部分である。
配列。pointerを使ってアクセス されることが多い。2次元配列と、pointerの配列の違いを理解しよう。
ユーザ定義可能な型。レコード型などと呼ばれることもある。 複数の型を合わせて一つのobjectとして扱うことができる。
分割コンパイルする場合は、 Cでは以下のようにすることが多い。
ただし、分割が進むと、大域関数とかを、すべてのファイルが使うとは限らない。 その時には、.c ファイルに対応した、.h を用意して、使うものだけど、#include するということになる。この時に、#include の順序に依存したり、2度 include したりしないように注意する必要がある。
定数の設定には、#define ではなく、enum を使う場合もある。この方が、 debugger が定数の表示を知ることができるので、debug しやすくなる。