自作言語malgoのコンパイラをリファクタリングした話

 自作言語malgoのコンパイラをリファクタリングした話

第29回 #hiro_it で発表した資料です。
リポジトリは https://github.com/takoeight0821/malgo

8666edb602e697bfb338231345e34cfb?s=128

Yuya Kono

August 30, 2020
Tweet

Transcript

  1. 自作言語malgoのコンパイラを リファクタリングした話 第29回 #hiro_it 星にゃーん(@takoeight0821)

  2. 自作言語malgo • ML系言語をベースに設計した極めてコンパクトな俺言語 ◦ MinCamlとPolyTigerの間の子 • 2016年頃からちまちま作っている ◦ 当時はコンパイラの作り方を全然知らなかった。感慨深い。 •

    多相型、関数リテラル、パターンマッチなどの言語機能を持つ ◦ MLらしく、変数や関数の宣言は let ~ in ~ ◦ カリー化は未実装 • C言語で書かれたプログラムと簡単にリンクできる ◦ ただし、C言語側でラッパー関数を書く必要がある
  3. malgoで書いたフィボナッチ数計算プログラム let extern print_int : Int -> {} = "print_int"

    in let extern newline : () -> {} = "newline" in let fun fib(n) : Int = if n <= 1 then 1 else fib(n - 1) + fib(n - 2) in print_int(fib(10)); newline()
  4. malgoの機能 • 変数の定義 let val x = 5 in ~

    • 関数の定義 let fun f(x) = x + 1 in ~ • 外部関数の定義 let extern print_int : Int -> {} = "print_int" • パターンマッチ match {1, 2} with | {x, y} => x + y • 無名関数 fun x => x + 1 • 配列 let val a = [1, 2, 3] in a.(1) <- 4; a.(0)
  5. malgoコンパイラ • Haskell製。コード数はだいたい3000行ぐらい • LLVM IRを生成する。アセンブルやリンクにはLLVMのツールチェーンを使う • 3つの中間表現を持つ ◦ HIR:すべての式が変数に束縛された形。

    ネストした式を持たない ◦ MIR:クロージャ(関数値)を、関数ポインタと環境の組に変換した形 ◦ LIR:多相関数を明示的な型変換でvoid*を使った単相関数に変換した形
  6. malgoコンパイラの問題点 1. 中間表現が多い 似ているようで違う4つの言語を扱う必要があり、機能追加が難しい 2. 最適化がLLVMまかせ GCが絡むコードの最適化がうまくいかない 3. 多相関数の扱いがややこしい コード生成パスにおけるバグの温床

    これらの問題点を解決するため、malgoとLLVM IRを結ぶ新たな中間表現を設計
  7. Core中間表現の設計 malgoとLLVM IRの間を結ぶ一つの中間表現を設計することに HaskellのコンパイラGHCの中間表現Coreから名前を拝借 Core中間表現に求められるのは次の3点 • ASTからの変換が容易 ◦ 複数の中間表現に渡る多段階の変換を回避 •

    ヒープに置かれる値とスタックに置かれる値の判別が容易 ◦ GCのアロケータ呼び出しの位置を特定できると最適化やコード生成が楽 • 多相的な値の表現が統一的 ◦ コード生成における型変換の挿入を削減
  8. 考えるべきこと:多相関数のサポート方法 多相関数をサポートする方法はいくつかある 1. 型ごとに異なる関数を生成する f(1); f('a'); => f_int(1); f_char('a'); 問題点:型の数に比例してコード量が増加する

    2. 値を「多相的な値」に変換する ←従来の実装 f(1); => any2int(f(int2any(1))); 問題点:すべての型について変換処理を定義しないといけない 3. すべての値を64bitにする ←Coreで採用 問題点:メモリを余分に消費し、場合によっては計算コストも生じる
  9. 参照型と値型 すべての値を64bitで表現するため、次のような表現を使う • 参照型:以下のような構造体へのポインタ • 値型:整数や文字などの値。64bitとは限らない 整数は以下のような参照型でラップする(文字なども同様) 8ビットのタグ(パターンマッチで使用) 任意長のペイロード Int#(整数型を表すタグ)

    (64bit符号付き整数)
  10. 改善:Core中間表現の設計 malgoとLLVM IRの間を結ぶ一つの中間表現を設計することに HaskellのコンパイラGHCの中間表現Coreから名前を拝借 Core中間表現に求められるのは次の3点 • ASTからの変換が容易 ◦ 複数の中間表現に渡る多段階の変換を回避 •

    ヒープに置かれる値とスタックに置かれる値の判別が容易 ◦ GCのアロケータ呼び出しの位置を特定できると最適化やコード生成が楽 • 多相的な値の表現が統一的 ◦ コード生成における型変換の挿入を削減 解決!
  11. 参照型の展開 参照型を使って計算するためには、ペイロードを取り出す必要がある Coreではパターンマッチを使ってペイロードを取り出せるようにする こんな式でペイロードを取り出すことにする match (式) with | Int# x

    -> x これはmalgoのパターンマッチの構文と対応している パターンマッチで取り出した値はスタック上に置かれる
  12. 参照型の導入 値を参照型でラップするためには、構造体をヒープ上に確保する必要がある Coreではlet式を使ってヒープ上に値を確保する let x = Int# 42 in x

    let式で定義した変数の値はヒープ上に確保される(ポインタはスタックに乗る) 関数や配列の定義もlet式で行う(クロージャや配列はヒープ上に確保するため)
  13. 改善:Core中間表現の設計 malgoとLLVM IRの間を結ぶ一つの中間表現を設計することに HaskellのコンパイラGHCの中間表現Coreから名前を拝借 Core中間表現に求められるのは次の3点 • ASTからの変換が容易 ◦ 複数の中間表現に渡る多段階の変換を回避 •

    ヒープに置かれる値とスタックに置かれる値の判別が容易 ◦ GCのアロケータ呼び出しの位置を特定できると最適化やコード生成が楽 • 多相的な値の表現が統一的 ◦ コード生成における型変換の挿入を削減 解決!
  14. ASTからCore、CoreからLLVM IRへの変換 • ASTとCoreはほぼ一対一で対応する ◦ ネストした式がない、ifがない、let valがmatchになるなどの細かな違い • CoreからLLVM IRの変換も難しくない

    ◦ パターンマッチはタグに対するswitch-caseとペイロードの取り出しに変換 ◦ 関数は基本的にすべてクロージャに変換 ◦ クロージャでない関数は直接呼び出すよう最適化 • Core上での最適化も実装 ◦ 関数と参照型によるラップのインライン展開 ◦ 冗長なletの除去
  15. malgoコンパイラのリファクタリング 1. 中間表現が多い。 => Core一つに! 2. 最適化がLLVMまかせ。 => Core上の最適化を実装! 3.

    多相関数の扱いがややこしい。 => 参照型による統一的な表現! 既存の設計の問題点を洗い出し、データ構造を検討し直すことで コードの品質を改善
  16. TODO:共通言語基盤としてのCore CoreはAny型とパターンマッチを持つ単純型付きラムダ計算(STL) • STLは関数型言語の理論的モデルとしてよく使われる malgoとは異なる言語のバックエンドとしても使えるはず • Coreへ「コンパイル」すればいい • 一から書くより楽に書ける ◦

    malgoのコードベースを再利用できるため
  17. Griff infixl 0 (|>) (|>) :: a -> (a ->

    b) -> b x |> f = f x if :: Bool -> {a} -> {a} -> a if c t f = c |> { True -> t! | False -> f! } forign import print_string :: String# -> () forign import newline :: () -> () putStrLn :: String -> () putStrLn = { String# str -> print_string str; newline () } 型検査を実装中