シリーズ一覧
- 【ストレンジコード】Filskaコンパイラ開発計画(1) 言語仕様を整理する【MLIR】
- 【ストレンジコード】Filskaコンパイラ開発計画(2) ソースコードをパースする【MLIR】
- 【ストレンジコード】Filskaコンパイラ開発計画(3) LLVM IRを出力する【MLIR】
- 【ストレンジコード】Filskaコンパイラ開発計画(4) 実行ファイル出力【MLIR】(本記事)
- 【ストレンジコード】Filskaコンパイラ開発計画(5) サブプログラムを複数作成可能にする【MLIR】
- 【ストレンジコード】Filskaコンパイラ開発計画(6) llvmir以外のdialectを経由したloweringを可能にする【MLIR】
TL; DR
- FilskaをMLIR製コンパイラへ移植
- MLIRとLLVMのCLIオプションが干渉して上手く動作しなかった
- 代わりにllcを使用しLLVM IRをバイナリに変換
はじめに
本シリーズでは、『ストレンジコード』に登場する難解プログラミング言語「Filska」をMLIRへ移植します。本家がPython製インタプリタなので、コンパイラにすることで高速化できるのでは?という淡い期待も寄せています。
(ストレンジコードにはFilska以外の難解プログラミング言語もたくさん登場します!言語好きは必見です (ダイマ))
前回ソースコードをLLVM IRに変換するところまで実装したので、今回はLLVM IRを実行ファイルへコンパイルします。
バイナリへのコンパイル処理を追加→失敗
作成中のFilskaコンパイラ(以下「filskalang」)は書籍『Learn LLVM 17』のtinylangをもとに作成しています。
tinylangではソースコードをLLVM IR経由でアセンブリ等へコンパイル可能です。その際に使用されるオプションが --filetype
で、指定した形式は llvm::codegen::getFileType()
から取得可能です。
ところが、filskalangで同様の実装を組み込んでも --filetype
オプションが使えるようになりませんでした。原因は分からないのですが、MLIR用のCLIオプションと干渉してしまったのかもしれません1。
また、仮に上記の問題を解決したとしても、出力されたアセンブリを別途 clang
で実行ファイルへコンパイルする必要があります。
代案: llcによるコンパイル
そこで、3つのコマンドでコンパイルを分担することにしました。Unix哲学
-
filskalang
: FilskaのソースコードをLLVM IRへコンパイル -
llc
: LLVM IRをアセンブリへコンパイル(LLVMの標準ツール) -
clang
: アセンブリを実行ファイルへコンパイル
$ ./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)
対処にはこちらの記事を参考にしました。
処理をスクリプト化
上記コマンドを毎回打ち込むのは辛いので、スクリプトにまとめています。
$ ./filskac sample.filska
# or
$ ./filskac -o sample sample.filska
で実行ファイルが得られます。ソースコードは以下の通りです。
#!/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言語の授業中にソースコードをバイナリで上書きし課題を吹き飛ばしてしまったトラウマからまだ立ち直れません
$ ./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)
コンパイルしてみる
{ main
set,10
prt
hlt
}
$ ./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のソースコードとの対応箇所もコメントされています。
アセンブリ(長いので折りたたみ)
.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:
おわりに
以上、Filskaコンパイラでバイナリを生成した方法の紹介でした。コンパイルの全工程ができたので、次回以降はサブプログラムの無限ループ、ジャンプを実装していく予定です。
-
tinylangではMLIRは扱っていません。 ↩