Jackコンパイラ作成(途中)

コンピュータシステムの理論と実装を続けてます。

今はJack言語のコンパイルを作成中で、構文解析を組んでいるところ。

ATSを作らずに、解析したすぐ側から構造をXMLに出力してたんだけど、それだと先読みが必要なノードでうまくいかないとわかって調整中。

具体的には、解析した構文木をツリー構造のデータと保持して、全て解析が終わってから出力しようとしてる。

この本が終わったらCコンパイラ作りにもう一度挑戦したい〜。以前挑戦した時はgdbの使い方よくわからなくてしんどかったんだよね。全部printfデバッグしたりして。。

Go言語に少し触れた

昨日、A Tour of Goを少しみてみた。

パラパラとめくりながらfor文あたりまで読んでみたけど、なかなか良いと思った。コンパイル言語っぽいし、身につけるのもありかな。

個人的に良いなと思った部分。

型宣言  識別子、型の順番に定義するところ。たしかに関数ポインタの書き方も可読性が高まるし、C言語とかによくある「型 識別子」の順番だと、宣言の理解時に視線が一旦戻る。(識別子Aは、int型のように識別子名を認識してからその型を把握してる気がする)

それならGoのように書いて貰えば、「identA (is) int (type).」のように人間が読みやすい語順かと思う。

var i int = 3

Named return values 名前付き返り値とでも訳すのかな? 返り値に名前をつけておけば関数内部で変数のように使えるし、returnで明示的に値を指定しなくても暗黙的に返してくれる。

If with a short statement if文の評価式で使うための変数宣言(とか、事前計算みたいなの)をif文でかける。Cでもif文の評価式で変数宣言と代入はできた気がするけど、一見わかりにくいし、if文の外で事前処理すると評価用の変数のスコープが広くなったりして良くない。

// BMIからメタボか判定する関数。本来BMIだけで判断するものではないけど。
func isMetabo(h, m float64) bool {
    // ↓このBMI変数みたいに、" 評価に使う文; 評価式"のようにかける
    if BMI := h * h / m; BMI > 25.0 {
        return true
    }
    return false
}

Switch-caseが値じゃなくてもいい 衝撃。文字列によって処理を切り分けたいとか当然あるわけだけど、Cだと似たようなif文がむちゃくちゃ並んで読みにくかった。文字列の内容だけの分岐で保てばまだいいけど、あとかた修正する人が文字列+アルファの条件を同じレベルに入れちゃって読みにくくなったりね。 goだとcase "文字列"みたいにかけるから、コードが読みやすいと思う。 (コンパイラ作るのに使いやすそう!)

switch keyword {
    case "umi":    // caseに文字列をかけるから見やすい?
        swim()  // 各caseには暗黙的にbreakが入る。
    case "kawa":
        swim()
    case "yama":
        climb()
}

とりあえずこのあたりくらいまで見てみた。面白そうだからもうちょっと読んでみようかな。

コンピュータシステムの理論と実装(3)

昨日はVMのStaticsTestを通すことができた。

StaticsTestはStaticセグメントへのリードライトのテストで、staticセグメントのスコープがちゃんと処理できているか?のテストだと思う。

初め、処理が全部終了しても、staticセグメントからスタックにとってきたはずのデータが0になっていて、なんでだーとなっていた。

staticセグメントは、ユニークラベルを作ってやると、アセンブラがラベルに遭遇した順にRAM上に割り付けてくれる。このラベルは「関数名.staticセグメントの番号」でつければ要件を満たしている…。

ただ実際はVMファイル内で共有アクセスできるから、関数名じゃなくてファイル名で付けないといけなかった。(でないと、ファイル内にある複数の関数から共通の領域を見れない)

コンピュータシステムの理論と実装(1)

昨日はプロジェクト8のFibonacciFunctionのテストが通った。2日くらいかかってしまった。

VMのeq、lt、gt命令の実装がバグってて、正しいアセンブラをはけてなかった。

次はstatic変数を使うテストみたい。何も考えずにテスト通してみたら通らなかった。また今日からデバッグする。

にしても、やっぱり低レイヤーのデバッグは辛いな。。。コメントなしで吐かれたアセンブラを解読するのはきつい。

いい加減デバッグコメントありのアセンブラを吐き出すオプションつけようかな。

ファイル読み切ったあとのstd::ifstream::seekg()がうまく動かない

少し前に悩んでました。 ファイル終端まで読み切るとeofフラグが立ってしまうから、そのままだとシークできないみたいです。

一旦ifstream::clear()をしてフラグを落としてやればシークできるようになりました。

Makefile書き方メモ

Makefileを書く上での個人的なメモ書きをしておきます。だんだん書いていきます。

以下の例では、空白をスペースで書いていますが、 Makefileはスペースではなくtab文字でないとかけないはずなので、 コピペしたい人は気をつけてください。
(写経するか、コピペしたあと半角スペースをtabに変えるなど…)

Makefileの関数

addprefix

第1引数の内容を、第2引数の先頭にprefixとして連結する関数。第2引数が複数の文字列からなる場合はそれぞれに連結する。

Makefile

OUTPUT    = $(addprefix AAA,BBB)

all:
    echo $(OUTPUT)

.PHONY: all

結果

$ make
echo AAABBB
AAABBB

「低レイヤを知りたい人のためのCコンパイラ作成入門」の進捗

低レイヤを知りたい人のためのCコンパイラ作成入門をまたぼちぼちやっていました。

github.com

前回から結構色々実装しました。

難しかったところとかコメントします。

コメント

関数呼び出し

呼び出すときのrspが16の倍数でないといけないので、その調整に苦労しました。 結局関数呼び出しのときにいちいち計算して調整しています。

ただこれはコンパイル時に決定できる気がする(関数先頭からスタックにpushした量を決めておけばいい)ので、実行時に分岐するのはやめにしたいです。

変数宣言

int型を追加して、型による変数定義を行えるようにしました。 ただ未だにrccではint型のサイズが8byteになっています。(64bitレジスタを使ったコンパイラをこれまで作ってきていて、変え方がわからないため)

どうも、raxレジスタの代わりにeaxレジスタとかを使ってやればいいみたいなんですが、まだ着手できていないです。

ポインタの計算

前述の通り、8byteになっちゃったint型しかないのでここは楽でした。(変数のサイズが8byte固定なので)。intがちゃんと4byteになれば、intかポインタかでアドレス計算の仕方を切り分ける必要があります。

たとえばアドレスは64bitなので、8byteですが、intは4byteなので、ポインタのポインタを計算するときと、intのポインタを計算するときで、+1したときの挙動が変わります。

配列

配列の伸び方をアドレスの負方向に伸ばしていたので、アドレス計算に手こずってました。スタックは負方向、ヒープは正方向に伸びているような計算の仕方していてうまくいきませんでした。

これはスタックとしては負方向に伸ばすけど、配列は正方向に伸びるようにすることで解決しました。

ex.)100番地から長さ4のintの配列を確保したとき、a[0]は112番地、a[1]は108番地…というように、スタック上の配列要素の配置箇所をどんどん若くしていく。

おわり

だんだんコンパイラっぽく動くようになってきて楽しいです。 たとえば↓のようなコードがコンパイルできます。

int main()
{
    int a[10];
    a[0] = 3;
    a[1] = 4;
    a[1+2*2] = 123;
    return a[5];
}
int add2(int a,int b)
{
    return a + b;
}
 
int main()
{
    return add2(10, 15);
}