daisuzz.log

"コンピュータシステムの理論と実装 第11章 コンパイラ:コード生成" を読んだ

「コンピュータシステムの理論と実装」を読んだメモを書いていきます。

前回のコンパイラ(字句解析器、構文解析器)の実装の記事は以下。

iikanji.hatenablog.jp

メモ

コンパイラ

  • フロントエンド
  • バックエンド
  • 高水準言語のソースコードの変換のステップ
    • データ変換
    • コマンド変換

データ変換

  • 対象の変数の型、ライフサイクル、スコープなどによって変換作業が決まる
    • スタックマシンへのpush/popの方法
    • OSのどのサブルーチンを利用するか
    • 格納するメモリ領域はヒープか、決められた領域か
  • シンボルテーブル
    • 対象の識別子についての情報を記録するテーブル
    • 変数名なのかクラス名なのかサブルーチン名なのか、引数かローカル変数かフィールドか、など
    • 同名の識別子はスコープによって別のものを指すこともあるため、シンボルテーブルは識別子のスコープも記録する
      • ハッシュテーブルのリストを使ってスコープを表現する
      • リストの要素は次の要素の中にネスト化されている
  • 変数操作
    • 一般的に、コンパイラを作る際にソースコードで宣言される変数を対象のプラットフォームにどう落とし込むかという問題がある
    • 変数の型によってメモリサイズやメモリへの割当て方が変わったり、変数のスコープやライフサイクルによってメモリの管理方法も変わる
    • Jackコンパイラでは7章8章で実装したバックエンド(VM)がメモリ割り当てを代わりに行う
    • VMがあることによって、コンパイラのフロントエンドは、変数を仮想メモリセグメントにマッピングして、高水準プログラムをVMコマンドに変換するだけで済む
  • 配列操作
    • 配列名がRAM上のベースアドレスを指す
    • Javaでは配列の宣言時にベースアドレス分のメモリスペースが割り当てられて、実行時に実際のサイズ分メモリスペースが割り当てられる
    • 動的メモリ割当てにはヒープが使われる
    • 配列宣言をコンパイルするとOSが提供しているallocなどのメモリ割当関数に変換される
  • オブジェクト操作
    • オブジェクト型の変数が宣言されると、オブジェクトのベースアドレスを格納するメモリ領域を確保する
    • コンストラクタが呼ばれるとインスタンスのフィールド分のメモリが確保される
    • オブジェクトのデータはベースアドレスからのインデックスを使ってアクセスする
    • メソッドはオブジェクトの各インスタンスが共通のものを使うため、見かけ上はインスタンスのメソッドを呼び出しているように見えるが、実際はメソッドの隠れ引数としてインスタンスを渡している

コマンド変換

  • 式の評価
    • 構文解析によって生成された構文木を走査することでVMコードを生成する
    • スタックベースのプラットフォームを対象とする場合逆ポーランド表記法で出力することで簡単に式を評価することができる
    • 走査のアルゴリズムはpost order traversalを利用する
  • フロー制御
    • if,switch,whileを条件付きgoto, 無条件gotoで表現する
    • ネストや複数のフロー制御に対応するためラベルをユニークにする

実装

前章で実装した字句解析器と構文解析器を改良してVM言語で書かれたコードを出力するコンパイラをKotlinで実装する。 実際に実装したソースコードは以下。

github.com

感想

今回の実装では、フィールドやコンンストラクタやメソッドなどオブジェクト指向の構文をVM言語に変換する点と、配列をVM言語に変換する点で苦戦した。 オブジェクト指向の構文の変換では、メソッド呼び出しの際にインスタンス変数をメソッドの第一引数として扱う必要があるため、メソッドの呼び出し側とメソッドの定義側でそれぞれlocalセグメントの値をpush する処理とargumentセグメントの値をpushする処理を埋め込むようコンパイラを改良した。

また、以下のように左辺と右辺両方に配列を利用している変換の場合、最初に左辺の配列のアドレスをTHISセグメントに代入するが、その後右辺の配列のアドレスでTHISセグメントが上書きされてしまうので、左辺の配列のアドレスがわからなくなってしまう。

// ...

let a[0] = b[0]
 
// ...

そのため左辺の配列のアドレスを計算した後、すぐにPOINTER 1に値をpopするのではなく一度TEMPセグメントに値を退避させて、右辺の値が計算できたあとでTEMPセグメントの値をPOINTER 1にpopするようコンパイラを改良した。 この際にpush/pop TEMP 0と記述していたが、OSのサブルーチンとして元々用意されていたVM関数の内部でTEMP 0をすでに利用していたため、うまくプログラムが動作せず2時間近くハマってしまった。

(追記) 2020年12月に原著の第2版がでるらしい www.amazon.com

次章はこちら

iikanji.hatenablog.jp