Haskell+LLVM構成で作る自作コンパイラ


Haskellのパーサコンビネータmegaparsecと、コンパイラ基盤であるLLVMを使って、コンパイラを作ってみます。モチベーションとしてはコンパイラはC/C++を使って作るのが定番ですが、型の恩恵を受けながら開発したいなあということでHaskellでも作れないか調べてみました。

# コンパイラ作成の流れ

1. パーサコンビネータのMegaparsecで四則演算のパーサを作成します。
2. LLVM IR構築のライブラリであるllvm-hs-pureでLLVM IRのコードへ変換するコンパイラを作成します。
3. 簡単な式をコンパイルしてLLVMのインタープリタ`lli`で実行します。
4. LLVMのコンパイラ`llc`でアセンブラを生成して`gcc`でバイナリまで生成します。

# 環境

あらかじめ以下をインストールしておいてください。

* stack 1.9.3
* Haskell 8.6.5
* LLVM 9.0.0

コマンドラインで以下を実行できればOKです。

```sh
$ stack --version
1.9.3 x86_64
$ lli --version
LLVM (http://llvm.org/):
  LLVM version 9.0.0
  Optimized build.
  Default target: x86_64-apple-darwin18.7.0
  Host CPU: skylake
```

# プロジェクトの作成

まずは新規プロジェクトを作成していきます。

```
$ stack init simple-calc
$ cd simple-calc
```

# 依存パッケージの追加

プロジェクトを作成したらプロジェクト直下に以下の2つのファイルたありますので、書き換えて依存ライブラリをインストールしてください。

```yaml:stack.yaml
# 以下、追記
extra-deps:
  - text-1.2.3.1
  - filepath-1.4.2.1
  - megaparsec-8.0.0
  - llvm-hs-pure-9.0.0
  - llvm-hs-pretty-0.6.2.0
```

```yaml:package.yaml
dependencies:
  - base >= 4.7 && < 5
  # 以下、追加
  - text == 1.2.3.1
  - filepath == 1.4.2.1
  - megaparsec == 8.0.0
  - llvm-hs-pure == 9.0.0
  - llvm-hs-pretty == 0.6.2.0
```

```sh
$ stack install
```

これで準備が整いました。
このあとの5つのソースコードを作成します。

* src/AST.hs
* src/Parser.hs
* src/Interpreter.hs
* src/Compiler.hs
* app/SCC.hs

また、replで書いてあるコード対話環境での実行を表しています。
`stack ghci`で対話環境を始めてください。
ソースコードの変更を反映する場合は`:reload`で、対話環境を終了するときは`:q`でできます。

```hs:repl
$ stack ghci
> :reload
> :q
```

# パーサを作る

早速パーサを作っていきます。

1. BNFの定義
2. ASTの実装
3. パーサの実装
4. 対話環境でテスト
5. 簡易インタープリタの作成

の順番で進めます。

## BNFの定義

まずはBNFを定義していきます。
今回作るコンパイラはただの四則演算の式だけを受け付けます。
なので下のような簡単なBNFを考えます。
`"1"`で囲まれた文字が終端記号で、`<Expr>`が非終端記号です。
その他の部分は正規表現的に表現しています。
（ちなみにこの定義だと右再帰ですね。右から演算が進みます）

```
<Expr> := <Term> | <Term> <ExprOp> <Expr>
<Term> := <Number> | <Number> <Number>
<Number> := ("1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"0")+
<ExprOp> := "+"|"-"
<TermOp> := "*"|"/"
```

## ASTの実装

抽象構文木をHaskellのデータで定義すると以下のようになります。
基本的にそのままです。

```hs:src/AST.hs
module AST where

data Expr
  = Expr Term ExprOp Expr
  | ExprTerm Term
  deriving (Show)
data Term
  = Term Number TermOp Term
  | TermNumber Number
  deriving (Show)
data Number = Number Integer deriving (Show)

data ExprOp = Add | Sub deriving (Show)
data TermOp = Mul | Div deriving (Show)
```

## パーサの実装

パーサを1から作るのは大変な作業なのでパーサライブラリの力を借りることにします。
Haskellのパーサジェネレータやパーサコンビネータにはいくつかライブラリがあり、もっとも有名なものが[Parsec](https://github.com/haskell/parsec)です。ParsecはAltJSの一つである[PureScript](https://github.com/purescript/purescript/)のトランスパイラ実装でも使われているライブラリです。

ですがParsecは現在はメンテナンスモードらしく積極的な開発はされていないとのことです。
そこでParsecよりアクティブなMegaparsecを利用したいと思います。
(参考: [Megaparsec vs Parsec](https://.com/mrkkrp/megaparsec#megaparsec-vs-parsec))

Megaparsecはパーサコンビネータと言って、小さいパーサを組み合わせてより大きいパーサを作るという方式を採用しています。小さいパーサを作りながら進めていきます。
チュートリアルは[こちら](https://haskell.e-bigmoon.com/posts/2019/07-14-megaparsec-tutorial.html)がわかりやすいです。もし全くPasecのようなパーサコンビネータを触ったことがない場合はこちらを先に一読することをオススメします。

### Numberのパーサ

まず、Numberのパーサを作ります。`1`や`100`を受理するパーサです。すでにMegaparsecが整数のパーサ`decimal`を作成してくれているので、それに`Number`を被せて返却するパーサを作ります。decimalは負の値は受理しません。

```hs
num :: Parser Number
num = Number <$> decimal
```

### Termのパーサ

`Term`のパーサを作ります。`1*2`や`3/4`を受理するパーサです。

先に演算子のみのパーサを作ります。

```hs
termOp :: Parser TermOp
termOp = choice
  [ Mul <$ char '*'
  , Div <$ char '/'
  ]
```

`termOp`は演算子はそれぞれ`*`なら`Mul`、`/`なら`Div`を返します。`choice`は`[A, B, ...]`の配列の中身を先頭から試し該当するものを選択してくれる関数です。

次に`Term`のパーサを作ります。

```hs
term :: Parser Term
term 
  = try (Term <$> num <*> termOp <*> term) 
  <|> TermNumber <$> num
```

`term`はいま作ったパーサをアプリカティブスタイルで適用しているものになります。
`A <|> B`はAまたはBのコードになることを意味します。ところが`A`で1文字以上読み進めて（消費して）から失敗すると`B`を実行する前にエラーでパースに失敗するというハマりポイントがあります[^1]。`try(A)`で囲むとAで失敗した場合に`B`が実行されるようになります。

[^1]: @as_capabl さん[ご指摘](https://qiita.com/toru0408/items/6bedee118ba08ea2c0b2#comment-d3c09fa541b38b3ba66f)ありがとうございます。詳細は[こちら](https://haskell.e-bigmoon.com/posts/2019/07-14-megaparsec-tutorial.html#Try)を確認ください。

### Exprのパーサ

`Expr`のパーサを作ります。`1+2`や`1-2`を受理するパーサです。`expr`と`exprOp`の実装は`term`と`termOp`と同じですね。

```hs
exprOp :: Parser ExprOp
exprOp = choice
  [ Add <$ char '+'
  , Sub <$ char '-'
  ]
```

```hs
expr :: Parser Expr
expr 
  = try (Expr <$> term <*> exprOp <*> expr) 
  <|> ExprTerm <$> term
```

### パーサの仕上げ

最後に一工夫必要です。

exprは文字列の先頭から正しい受理できるところまでしか読み取りを行ってくれません。
最後まで読み取ってくれるようにEOFまで消費する`prog`を実装します。

```
prog :: Parser Expr
prog = expr <* eof
```

それからコメントや空白をスキップするようにしましょう。

```hs
scn :: Parser ()
scn = L.space
  space1
  (L.skipLineComment "//")
  (L.skipBlockComment "/*" "*/")
```

これをトークンごとにlexemeで囲ってあげるとトークンの後ろの空白を消費してくれます。

```hs
num :: Parser Number
num = L.lexeme scn $ Number <$> L.decimal
```

トークンごとにつけても先頭の空白はスキップしてくれないので、以下のようにつけてあげるのも忘れないようにします。

```hs
prog :: Parser Expr
prog = scn *> expr <* eof
```

### 完成したパーサ

完成したパーサの全体は以下の通りです。

```hs:src/Parser.hs
{-# LANGUAGE OverloadedStrings #-}

module Parser where

import Data.Text
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import AST

type Parser = Parsec Void Text

prog :: Parser Expr
prog = scn *> expr <* eof

expr :: Parser Expr
expr
  = try (Expr <$> term <*> exprOp <*> expr)
  <|> ExprTerm <$> term

term :: Parser Term
term
  = try (Term <$> num <*> termOp <*> term)
  <|> TermNumber <$> num

num :: Parser Number
num = L.lexeme scn $ Number <$> L.decimal

exprOp :: Parser ExprOp
exprOp = L.lexeme scn $ choice
  [ Add <$ char '+'
  , Sub <$ char '-'
  ]

termOp :: Parser TermOp
termOp = L.lexeme scn $ choice
  [ Mul <$ char '*'
  , Div <$ char '/'
  ]

scn :: Parser ()
scn = L.space
  space1
  (L.skipLineComment "//")
  (L.skipBlockComment "/*" "*/")
```

### 試しに実行

対話環境で試してみましょう。

```hs:repl
$ stack ghci
> import Text.Megaparsec (parseTest)
> parseTest expr "1"

<interactive>:2:16: error:
    • Couldn't match expected type ‘Data.Text.Internal.Text’
                  with actual type ‘[Char]’
    • In the second argument of ‘parseTest’, namely ‘"1"’
      In the expression: parseTest expr "1"
      In an equation for ‘it’: it = parseTest expr "1"
```

おっと。パーサはStringではなくTextで定義したんでしたね。
`pack :: String -> Text`でText型に変換

```hs:repl
> import Data.Text (pack)
> parseTest expr (pack "1")
ExprTerm (TermNumber (Number 1))
> parseTest expr $ pack "1*2"
ExprTerm (Term (Number 1) Mul (TermNumber (Number 2)))
> parseTest expr $ pack "1*3+2/4"
Expr (Term (Number 1) Mul (TermNumber (Number 3))) Add (ExprTerm (Term (Number 2) Div (TermNumber (Number 4))))
```

少し読みづらいですがうまくパースできていそうですね。

## 簡易インタープリタを実装

ASTができたのでこれをLLVM IRに変換すればコンパイラができますね。
ですが、ここで少し寄り道して実際に計算ができることを確認するためにインタープリタを作ってみましょう。
`interpreter`の型は単純で式を受け取って数値に変換`Expr -> Integer`するものとしましょう。
今回のExprとTermとNumはすべてIntegerにできますから評価できるという意味で`Evaluable`クラスに属させることにしましょう。Evaluableクラスは`eval :: a -> Integer`関数のみを定義します。

```hs:src/Interpreter.hs
module Interpreter (interprete) where

import AST

interprete :: Expr -> Integer
interprete = eval

class Eval a where
  eval :: a -> Integer

instance Eval Expr where
  eval (ExprTerm t) = eval t
  eval (Expr t Add e) = (eval t) + (eval e)
  eval (Expr t Sub e) = (eval t) - (eval e)

instance Eval Term where
  eval (TermNumber n) = eval n
  eval (Term n Mul t) = (eval n) * (eval t)
  eval (Term n Div t) = (eval n) `div` (eval t)

instance Eval Number where
  eval (Number n) = n
```

```hs
> interprete <$> parse prog "" (pack "1*3+2/4")
Right 3
```

# コンパイラを作る

llvm-hs-pureというライブラリを使うとLLVM IRのASTを簡単に構築することができます。
ちなみにllvm-hs-pureはLLVMのC++をラップしたFFIではなく、llvmがインストールされていなくとも利用できます。FFIのllvm-hsというライブラリもあります。ですが、llvmを[ソースからビルドしないとうまく使えない](https://github.com/llvm-hs/llvm-hs/issues/241)などやや環境構築が大変です。

どのようなコンパイラを作るかというと四則演算のASTをLLVM IRのAST(`Module`)に変換するというものを考えます。LLVMのASTを作ると簡単に具象構文に直すことができます。なのでAST（四則演算）をAST（LLVM IR）に変換するデータ変換器をつくるようなイメージです。

LLVMのモジュールは関数やオペランドなどから構成されますが、今回は単純な式だけを扱うので、`Expr`を`Operand`に変換していくということを考えていきましょう。
まずはLLVMのオペランド`Operand`にできるという型クラスを作成します。

```hs
class LLVMOperand a where
  toOperand :: a -> LLVMBuilder Operand
```

`Expr`、`Term`、`Number`のそれぞれはLLVMOperandの型クラスのインスタンスにしてみます。

## Numberのコンパイル

まずは`Number`です。

```hs
instance LLVMOperand AST.Number where
  toOperand (AST.Number n) = return (int32 n)
```

`int32 :: Integer -> Operand`で単純にIntegerをOperandに変換してくれるという関数です。

その他の単純なオペランドへの変換は[`LLVM.IRBuilder.Constant`](http://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-IRBuilder-Constant.html
)で定義されています。

## Termのコンパイル

次に`Term`のインスタンスを定義してみます。

```hs
instance LLVMOperand AST.Term where
  toOperand (AST.TermNumber n) = toOperand n
  toOperand (AST.Term n AST.Mul t) = mdo
    n' <- toOperand n -- Operandに変換する
    t' <- toOperand t
    mul n' t' -- mul命令を適用する。適用した結果はOperandを返す。 
  toOperand (AST.Term n AST.Div t) = mdo
    n' <- toOperand n
    t' <- toOperand t
    sdiv n' t' -- 整数の商を返す。
```

それぞれ`toOperand`してから`mul`や`sdiv`といった命令に適用しています。これで再帰的にOperandに変換していくことができます。
命令`mul`や`sdiv`は`MonadIRBuilder m => Operand -> Operand -> m Operand`のような型となっています。

## Exprのコンパイル

最後に`Expr`のインスタンスを定義します。

```hs
instance LLVMOperand AST.Expr where
  toOperand (AST.ExprTerm e) = toOperand e
  toOperand (AST.Expr t AST.Add e) = mdo
    t' <- toOperand t
    e' <- toOperand e
    add t' e'
  toOperand (AST.Expr t AST.Sub e) = mdo
    t' <- toOperand t
    e' <- toOperand e
    sub t' e'
```

これは`Term`と同じですね。

## 計算結果の表示

計算結果を標準出力に表示する部分について考えます。
`printf`を使うことにしましょう。

まずは`printf`関数を使うことを宣言します。

```hs
printf <- externVarArgs "printf" [ptr i8] i32
```

`externVarArgs`は可変長引数である外部変数の定義です。引数が固定の場合は`extern`を使います。
これはコンパイルされると以下のようなコードになります。

```llvm
declare external ccc i32 @printf(i8*, ...)
```

`printf`をリンカが解決してくれます。

それから`printf`を呼び出して使うための処理を行います。ちなみにC言語で書くと次のようなコードを書きたいということになります。

```c
int r = 1 + 1;
printf("%d\n", r);
```

まずは`"%d\n"`を作ってみます。LLVM IRはやはり低レベルなので文字列を作るのも大変なのですが、これには[globalStringPtr](https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-IRBuilder-Instruction.html#v:globalStringPtr)というぴったりの関数がありました。globalStringPtrの説明には次のように書いてあります。

> Creates a series of instructions to generate a pointer to a string constant. Useful for making format strings to pass to printf, for example

直訳すると「文字列定数のポインタを作るための一連の命令を作ります。例えば、printfに渡すフォーマット文字列を作るのに便利です。」というような感じになりますね。

```hs
form <- globalStringPtr "%d\n" "putNumForm"
```

それから関数を呼び出す必要がありますね。関数呼び出しは文字通り[call](https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-IRBuilder-Instruction.html#v:call)関数を使います。

```
call :: MonadIRBuilder m 
  => Operand -- 関数
  -> [(Operand, [ParameterAttribute])] -- 指定した関数に対する引数
  -> m Operand -- 指定した関数の戻り値
````

基本的には直感的なシグネチャですね。
[ParameterAttribute](http://llvm.org/docs/LangRef.html#parameter-attributes)は引数に関する追加情報を与えることができるようです。
パッとみる感じでは厳密な制約を指示するための属性のようです。ここでは何も与えないことにしようと思います。

```hs
call printf [(form, []), (r, [])]
```

いい感じですが、型が合いません。
`form`は`Constant`型です。`Operand`型でなくてはなりません。
実は私はこれにだいぶハマってしましました。なんとかリファレンスを漁ってみると[Operand](https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-AST-Operand.html#t:Operand)のコンストラクタに`ConstantOperand`がありました。`Constant`はそのまま`Operand`にできるんですね。

```hs
call printf [(ConstantOperand form, []), (r, [])]
```

これで呼び出しOKです。

## main関数の構築

ということで最後にmain関数を作ります。

```hs
compile :: AST.Expr -> Text
compile expr = ppllvm $ buildModule "main" $ mdo
  form <- globalStringPtr "%d\n" "putNumForm"
  printf <- externVarArgs "printf" [ptr i8] i32
  function "main" [] i32 $ \[] -> mdo
    entry <- block `named` "entry"; mdo
      r <- toOperand expr
      call printf [(ConstantOperand form, []), (r, [])]
      ret (int32 0)
```

## 完成したコンパイラ

```hs:src/Compiler.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecursiveDo #-}

module Compiler where

import Data.Text.Internal.Lazy
import Data.Functor.Identity

import LLVM.Pretty
import LLVM.AST hiding (function, value)
import LLVM.AST.Type as AST

import LLVM.IRBuilder.Module
import LLVM.IRBuilder.Monad
import LLVM.IRBuilder.Instruction
import LLVM.IRBuilder.Constant

import qualified AST

type LLVMBuilder = IRBuilderT (ModuleBuilderT Identity)

compile :: AST.Expr -> Text
compile expr = ppllvm $ buildModule "main" $ mdo
  form <- globalStringPtr "%d\n" "putNumForm"
  printf <- externVarArgs "printf" [ptr i8] i32
  function "main" [] i32 $ \[] -> mdo
    entry <- block `named` "entry"; mdo
      r <- toOperand expr
      call printf [(ConstantOperand form, []), (r, [])]
      ret (int32 0)

class LLVMOperand a where
  toOperand :: a -> LLVMBuilder Operand

instance LLVMOperand AST.Expr where
  toOperand (AST.ExprTerm e) = toOperand e
  toOperand (AST.Expr t AST.Add e) = mdo
    t' <- toOperand t
    e' <- toOperand e
    add t' e'
  toOperand (AST.Expr t AST.Sub e) = mdo
    t' <- toOperand t
    e' <- toOperand e
    sub t' e'

instance LLVMOperand AST.Term where
  toOperand (AST.TermNumber n) = toOperand n
  toOperand (AST.Term n AST.Mul t) = mdo
    n' <- toOperand n
    t' <- toOperand t
    mul n' t'
  toOperand (AST.Term n AST.Div t) = mdo
    n' <- toOperand n
    t' <- toOperand t
    sdiv n' t'

instance LLVMOperand AST.Number where
  toOperand (AST.Number n) = return (int32 n)
```

## コンパイラのエントリポイント

最後にコマンドラインからコンパイラを呼べるよう`main`を定義します。
ちなみにソースファイルの拡張子は.scにしましょう。(simple-calcなので)

```app/SCC.hs
module SCC where

import System.Environment (getArgs)
import System.FilePath.Posix (replaceExtension)
import qualified Data.Text.IO as T
import qualified Data.Text.Lazy.IO as LT
import Text.Megaparsec (parse)
import Text.Megaparsec.Error (errorBundlePretty)
import Compiler
import Parser

main :: IO ()
main = do
  args <- getArgs
  let srcPath = args !! 0
  let distPath = replaceExtension srcPath ".ll"
  src <- T.readFile srcPath
  let result = parse prog "example" src
  case result of
    Right ast -> LT.writeFile distPath (compile ast)
    Left e -> putStrLn $ errorBundlePretty e
```

これで完成で実装は完了です！

# コンパイラのテスト

## コンパイラのコンパイル

ではコンパイラをコンパイルしましょう。

```sh
$ stack build
```

## 四則演算のソースファイルの作成

さて、ソースファイルを作成します。

```sh
$ mkdir example
$ echo "1+2*3" >> example/ex.sc
$ cat example/ex.sc
1+2*3
```

## LLVM IRへコンパイル

LLVM IRにコンパイルしてみましょう。

```sh
$ stack runghc app/SCC.hs example/ex.sc
```

このようにするとex.llファイルが出力されています。

```sh
$ cat example/ex.ll
```

ちゃんとコンパイルできていますね。

```llvm:example.ll
; ModuleID = 'main'

@putNumForm =  unnamed_addr  constant [4 x i8] c"%d\0a\00"
declare external ccc i32 @printf(i8*, ...)    

define external ccc i32 @main() {
  entry_0:
    %0 = mul i32 2, 3 
    %1 = add i32 1, %0 
    %2 = call ccc  i32 (i8*, ...) @printf(i8*  getelementptr inbounds ([4 x i8], [4 x i8]* @putNumForm, i32 0, i32 0), i32  %1)  
    ret i32 0 
}
```

いったんバイナリを作る前に`lli`コマンドで試してみましょう。

```sh
$ lli example/ex.ll
7
```

おお！うまくいきました。

## llcとgccでバイナリの生成

```sh
$ llc example/ex.ll
$ gcc example/ex.s -o example/ex
```

これでバイナリexample/exができました。
実行してみます。

```sh
$ ./example/ex
7
```

ちゃんと実行できてますね！
お疲れ様でした。

# まとめ

HaskellとLLVMを使っている資料が少なくとても苦労しました。ですが、ここまで分かれば本格的なコンパイラもしっかり作れそうですね。調べてみた結果、（特に環境構築関連でハマると予想してましたが）思ったよりも割とすんなり動きましたし自分の中ではいい選択肢になりました。