【ストレンジコード】Filskaコンパイラ開発計画(4) 実行ファイル出力【MLIR】

シリーズ一覧

- [【ストレンジコード】Filskaコンパイラ開発計画(1) 言語仕様を整理する【MLIR】](https://qiita.com/Syuparn/items/e8f4046c4a5b551db310)
- [【ストレンジコード】Filskaコンパイラ開発計画(2) ソースコードをパースする【MLIR】](https://qiita.com/Syuparn/items/ca502ec32aded84ebac1)
- [【ストレンジコード】Filskaコンパイラ開発計画(3) LLVM IRを出力する【MLIR】](https://qiita.com/Syuparn/items/1b6fc01bbe7370578756)
- 【ストレンジコード】Filskaコンパイラ開発計画(4) 実行ファイル出力【MLIR】(本記事)
- [【ストレンジコード】Filskaコンパイラ開発計画(5) サブプログラムを複数作成可能にする【MLIR】](https://qiita.com/Syuparn/items/35ee40e8b77bb8657cee)
- [【ストレンジコード】Filskaコンパイラ開発計画(6) llvmir以外のdialectを経由したloweringを可能にする【MLIR】](https://qiita.com/drafts/0c327b5af5bfed62e4d0)

# TL; DR

- [Filska](https://esolangs.org/wiki/Filska)をMLIR製コンパイラへ移植
- MLIRとLLVMのCLIオプションが干渉して上手く動作しなかった
- 代わりにllcを使用しLLVM IRをバイナリに変換

https://github.com/Syuparn/filskalang

# はじめに

本シリーズでは、『ストレンジコード』に登場する難解プログラミング言語「[Filska](https://esolangs.org/wiki/Filska)」をMLIRへ移植します。本家がPython製インタプリタなので、コンパイラにすることで高速化できるのでは？という淡い期待も寄せています。

（ストレンジコードにはFilska以外の難解プログラミング言語もたくさん登場します！言語好きは必見です ~~（ダイマ）~~）

https://www.shuwasystem.co.jp/book/9784798069746.html

前回ソースコードをLLVM IRに変換するところまで実装したので、今回はLLVM IRを実行ファイルへコンパイルします。

# バイナリへのコンパイル処理を追加→失敗
作成中のFilskaコンパイラ(以下「filskalang」)は書籍『Learn LLVM 17』のtinylangをもとに作成しています。

tinylangではソースコードをLLVM IR経由でアセンブリ等へコンパイル可能です。その際に使用されるオプションが `--filetype` で、指定した形式は `llvm::codegen::getFileType()` から取得可能です。

https://github.com/PacktPublishing/Learn-LLVM-17/blob/main/Chapter05/tinylang/tools/driver/Driver.cpp#L14

ところが、filskalangで同様の実装を組み込んでも `--filetype` オプションが使えるようになりませんでした。原因は分からないのですが、MLIR用のCLIオプションと干渉してしまったのかもしれません[^cli]。

また、仮に上記の問題を解決したとしても、出力されたアセンブリを別途 `clang` で実行ファイルへコンパイルする必要があります。

# 代案: llcによるコンパイル
そこで、3つのコマンドでコンパイルを分担することにしました。~~Unix哲学~~

- `filskalang`: FilskaのソースコードをLLVM IRへコンパイル
- `llc`: LLVM IRをアセンブリへコンパイル（LLVMの標準ツール）
- `clang`: アセンブリを実行ファイルへコンパイル

```bash
$ ./bin/filskalang sample.filska -emit llvm > sample.filska.llir
$ llc sample.filska.llir -o sample.filska.llir.s
$ clang -no-pie sample.filska.llir.s sample
```

注意点として、`clang` のオプション `-no-pie` が無いと以下のエラーが発生してしまいます。

```
/usr/bin/ld: /tmp/foo-208b45.o: relocation R_X86_64_32 against `.rodata' can not be used when making a PIE object; recompile with -fPIE
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```

対処にはこちらの記事を参考にしました。

https://keens.github.io/blog/2016/11/30/sml_woubuntu_16_10deugokasu/

## 処理をスクリプト化

上記コマンドを毎回打ち込むのは辛いので、スクリプトにまとめています。

```bash
$ ./filskac sample.filska
# or
$ ./filskac -o sample sample.filska
```

で実行ファイルが得られます。ソースコードは以下の通りです。

```bash:filskac
#!/bin/bash

set -e

function usage() {
    echo "Filska compiler"
    echo "filskac [-o sample] sample.filska"
} 

while getopts "o:h" OPT
do
    case $OPT in
        o) OUTPUT=true;OUTPUT_FILE_OVERRIDE=$OPTARG;;
        h) HELP=true;;
    esac
done
shift $(($OPTIND - 1))


SRC_FILE_NAME=$1

if [ -n "$HELP" ]; then
    usage
    exit 0
fi

if [ -z "$SRC_FILE_NAME" ]; then
    echo "error: source file must be specified"
    usage
    exit 1
fi

OUTPUT_FILE_NAME=${SRC_FILE_NAME%.filska}
if [ -n "$OUTPUT_FILE_OVERRIDE" ]; then
    OUTPUT_FILE_NAME=$OUTPUT_FILE_OVERRIDE
fi

if [ "$OUTPUT_FILE_NAME" = "$SRC_FILE_NAME" ]; then
    echo "error: stop compiling because the output file name '$OUTPUT_FILE_NAME' is same as the src file name"
    echo "make sure that src file name is Filska source code (*.filska)"
    exit 1
fi

LLIR_FILE_NAME=${SRC_FILE_NAME}.llir
ASSEMBLY_FILE_NAME=${LLIR_FILE_NAME}.s

./bin/filskalang $SRC_FILE_NAME -emit llvm > $LLIR_FILE_NAME
llc $LLIR_FILE_NAME -o $ASSEMBLY_FILE_NAME
clang -no-pie $ASSEMBLY_FILE_NAME -o $OUTPUT_FILE_NAME
```

本題とは関係ありませんが、バイナリとソースコードが同名の場合にエラーになるようにしました。
~~学生時代、C言語の授業中にソースコードをバイナリで上書きし課題を吹き飛ばしてしまったトラウマからまだ立ち直れません~~

```bash
$ ./filskac -o tests/src/compile/dummy tests/src/compile/dummy
error: stop compiling because the output file name 'tests/src/compile/dummy' is same as the src file name
make sure that src file name is Filska source code (*.filska)
```

# コンパイルしてみる

```filska:example/simple.filska
{ main
    set,10
    prt
    hlt
}
```

```bash
$ ./filskac example/simple.filska
$ ./example/simple
10.000000
```

対応しているのは3命令だけとはいえ、ついにFilskaをネイティブコードまでコンパイルすることができました！

バイナリのサイズは17kBと思っていたより大きかったです。

```
$ ls -lh example/simple
-rwxr-xr-x 1 syuparn syuparn 17K May  6 21:48 example/simple
```

中間生成物のアセンブリは以下の通りです。
Filskaのソースコードとの対応箇所もコメントされています。

<details><summary>アセンブリ（長いので折りたたみ）</summary>

```
	.text
	.file	"LLVMDialectModule"
	.section	.rodata.cst8,"aM",@progbits,8
	.p2align	3, 0x0                          # -- Begin function main
.LCPI0_0:
	.quad	0x4024000000000000              # double 10
	.text
	.globl	main
	.p2align	4, 0x90
	.type	main,@function
main:                                   # @main
.Lfunc_begin0:
	.file	1 "example" "simple.filska"
	.loc	1 7 0                           # simple.filska:7:0
	.cfi_startproc
# %bb.0:
	pushq	%rax
	.cfi_def_cfa_offset 16
	movabsq	$4621819117588971520, %rax      # imm = 0x4024000000000000
.Ltmp0:
	.loc	1 2 8 prologue_end              # simple.filska:2:8
	movq	%rax, (%rsp)
	movsd	.LCPI0_0(%rip), %xmm0           # xmm0 = mem[0],zero
	.loc	1 3 8                           # simple.filska:3:8
	movl	$frmt_spec, %edi
	movb	$1, %al
	callq	printf@PLT
	.loc	1 1 7 epilogue_begin            # simple.filska:1:7
	popq	%rax
	.cfi_def_cfa_offset 8
	retq
.Ltmp1:
.Lfunc_end0:
	.size	main, .Lfunc_end0-main
	.cfi_endproc
                                        # -- End function
	.type	frmt_spec,@object               # @frmt_spec
	.section	.rodata,"a",@progbits
frmt_spec:
	.asciz	"%f"
	.size	frmt_spec, 3

	.section	.debug_abbrev,"",@progbits
	.byte	1                               # Abbreviation Code
	.byte	17                              # DW_TAG_compile_unit
	.byte	0                               # DW_CHILDREN_no
	.byte	37                              # DW_AT_producer
	.byte	14                              # DW_FORM_strp
	.byte	19                              # DW_AT_language
	.byte	5                               # DW_FORM_data2
	.byte	3                               # DW_AT_name
	.byte	14                              # DW_FORM_strp
	.byte	16                              # DW_AT_stmt_list
	.byte	23                              # DW_FORM_sec_offset
	.byte	17                              # DW_AT_low_pc
	.byte	1                               # DW_FORM_addr
	.byte	18                              # DW_AT_high_pc
	.byte	6                               # DW_FORM_data4
	.byte	0                               # EOM(1)
	.byte	0                               # EOM(2)
	.byte	0                               # EOM(3)
	.section	.debug_info,"",@progbits
.Lcu_begin0:
	.long	.Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit
.Ldebug_info_start0:
	.short	4                               # DWARF version number
	.long	.debug_abbrev                   # Offset Into Abbrev. Section
	.byte	8                               # Address Size (in bytes)
	.byte	1                               # Abbrev [1] 0xb:0x1b DW_TAG_compile_unit
	.long	.Linfo_string0                  # DW_AT_producer
	.short	2                               # DW_AT_language
	.long	.Linfo_string1                  # DW_AT_name
	.long	.Lline_table_start0             # DW_AT_stmt_list
	.quad	.Lfunc_begin0                   # DW_AT_low_pc
	.long	.Lfunc_end0-.Lfunc_begin0       # DW_AT_high_pc
.Ldebug_info_end0:
	.section	.debug_str,"MS",@progbits,1
.Linfo_string0:
	.asciz	"MLIR"                          # string offset=0
.Linfo_string1:
	.asciz	"<unknown>"                     # string offset=5
	.section	".note.GNU-stack","",@progbits
	.section	.debug_line,"",@progbits
.Lline_table_start0:
```

</details>

# おわりに
以上、Filskaコンパイラでバイナリを生成した方法の紹介でした。コンパイルの全工程ができたので、次回以降はサブプログラムの無限ループ、ジャンプを実装していく予定です。

[^cli]: tinylangではMLIRは扱っていません。