[scheme]Scheme Compiler の勉強(18)
Scheme で Scheme を書く場合、簡単にテストデータが作れるのが嬉しい。
※現在作成中の勉強用 scheme コンパイラ(というのもおこがましいが)は、直接 LLVM IR を吐き出すプリミティブしかなく、そしてまだ任意の足し算すらできない。またプリミティブな手続きは全て引数1つ。
たとえば 「0 に 1を足してその結果についてまた1を足して、、、を10回繰り返して結果が10になること」などというテストは、以下のように fold と make-list を使うと手が楽だ。
gosh> (fold list 0 (make-list 10 '$fxadd1)) ($fxadd1 ($fxadd1 ($fxadd1 ($fxadd1 ($fxadd1 ($fxadd1 ($fxadd1 ($fxadd1 ($fxadd1 ($fxadd1 0)))))))))) gosh> (run-compile (fold list 0 (make-list 10 '$fxadd1))) #<undef>
で、いまの LLVM 用自作「コンパイラ」は、上の S式を「コンパイル」するときはひたすらに1を足す LLVM IR のコードを積み重ねていく。そのままだと冗長だけど、いったん llvm-as コマンドで中間コードのバイナリを生成し、opt コマンドで最適化して新しい中間コードのバイナリを作成し、さらに llc コマンドで最終的なアセンブラを出力すると下のように簡潔なものになっている。(※以下は make-list に 1000 を与えている。"4000" になっているのは、32bit の 2bit をタグに使っているためその分ずれているから)
.globl _scheme_entry .align 4 _scheme_entry: li r3, 4000 blr
上は最終のアセンブラだけど、その一歩手前の、最適化された中間コードのバイナリも llvm-dis コマンドで参照できる。わたしの「コンパイラ」は3000行位の LLVM IR を出力しているので、それに比べれば随分コンパクト。
define i32 @scheme_entry() nounwind { entry: ret i32 4000 }
手続きが定義できるようになるまでには、この最適化された LLVM IR も最終のアセンブラも同時に簡単に参照できるように整備しておこう。gosh 環境から対話的にコンパイルしチェックを繰り返すようなイメージ。
定数畳み込み(constant folding)と定数伝播(constant propagation)
http://ja.wikipedia.org/wiki/コンパイラ最適化
定数からなる式(例えば、"3 + 5")をコンパイル時に計算結果("8")と置き換えてしまう。最近の言語処理系ではほとんど必ず行われる。
『「最近の処理系ではほとんど必ず」だって?そんなのは「未来」のコンパイラには関係ないッ!他の誰か(LLVMとか)にお願いすればいいんだッ!!』と言いたいところだけども、残念ながらそんなに単純では無いはず。それでも、LLVM みたいな中間層には未来を感じるのは確か。コンピューターの歴史は中間層の歴史、と昔上司にならったなぁ。
Compilers: Backend to Frontend and Back to Front Again: 1.4 Conditional Expressions
if の実装、といっても、そもそも LLVM IR にはそんなに命令の種類が無いので、'br' を使えばいいと分かる。if だと条件を満たさないときのコードは実行してはいけないので、今までのように順番に命令を実行するだけでは駄目。そのために 'br' とラベルを使う。
http://llvm.org/docs/LangRef.html#identifiers を読む。ラベルも参照するときは % がつくのか。おっと、 unnamed variable なんて便利そうなものがあるよ。そのうち使えると奇麗かもしれない。
if のメイン部分はこんな感じ。
(define (emit-if expr) (let ((%if-test (unique-var)) (%tmp (unique-var)) (L_if-true (unique-label)) (L_if-else (unique-label)) (L_if-cont (unique-label))) (emit-expr (if-test expr)) (emit " ~a = load i32* %ret" %if-test) ;; compare to #f (emit " ~a = icmp eq i32 ~a, ~a" %tmp %if-test bool_f) (emit " br i1 ~a, label %~a, label %~a" %tmp L_if-else L_if-true) (emit "~a:" L_if-true) (emit-expr (if-then expr)) (emit " br label %~a" L_if-cont) (emit "~a:" L_if-else) (emit-expr (if-else expr)) (emit " br label %~a" L_if-cont) (emit "~a:" L_if-cont)))
gosh> (compile-program '(if #f 12 9)) define i32 @scheme_entry() nounwind { entry: %ret = alloca i32 store i32 47, i32* %ret %var128 = load i32* %ret %var129 = icmp eq i32 %var128, 47 br i1 %var129, label %Label_80, label %Label_79 Label_79: store i32 48, i32* %ret br label %Label_81 Label_80: store i32 36, i32* %ret br label %Label_81 Label_81: %retval = load i32* %ret ret i32 %retval } #<undef>
gauche の disasm を使ってみると自作のコンパイラもどきの冗長さが際立つ。
gosh> (compile-program '(if #t (if 12 13 4) 17)) define i32 @scheme_entry() nounwind { entry: %ret = alloca i32 store i32 111, i32* %ret %var130 = load i32* %ret %var131 = icmp eq i32 %var130, 47 br i1 %var131, label %Label_83, label %Label_82 Label_82: store i32 48, i32* %ret %var132 = load i32* %ret %var133 = icmp eq i32 %var132, 47 br i1 %var133, label %Label_86, label %Label_85 Label_85: store i32 52, i32* %ret br label %Label_87 Label_86: store i32 16, i32* %ret br label %Label_87 Label_87: br label %Label_84 Label_83: store i32 68, i32* %ret br label %Label_84 Label_84: %retval = load i32* %ret ret i32 %retval } #<undef> gosh> (disasm (lambda () (if #t (if 12 13 4) 17))) main_code (name=#f, code=0xb5d58, size=13, const=0, stack=0): args: #f 0 CONST #t 2 BF 11 ; (if #t (if 12 13 4) 17) 4 CONSTI(12) 5 BF 9 ; (if 12 13 4) 7 CONSTI(13) 8 RET 9 CONSTI(4) 10 RET 11 CONSTI(17) 12 RET #<undef>
でもともかくも tests-1.4-req.scm まで全テストが通っている。
p. 17 に and , or 、それから現在のコンパイラの非効率さについて。後でやるか飛ばすか。
次の 1.5 Binary Primitives はちょっと LLVM のことを調べないと進めない印象。
続く。
小まとめ
※"Compilers: Backend to Frontend and Back to Front Again" ばっかり見てたけど、"An Incremental Approach to Compiler Construction" の方が今の知識で読むと読みやすい。
paper | わたしの勉強 | メモ | |
---|---|---|---|
Source Language | Scheme サブセット | Scheme サブセット | コンパイルする対象となる言語。 |
Implementation Language | Scheme (ChezScheme 7) | Scheme (gauche 0.8.8) | コンパイラを実装する言語。 |
Taget Architecture | Intel-X86 | PowerPC(LLVM 2.3)(PowerBook G4) | コンパイラのターゲットとなるプラットフォーム。 |
CISC | RISC-like | 命令セット。 | |
Help | gcc | llvm-gcc | 困った時に助けを求める別のコンパイラ。 |
Compilers: Backend to Frontend and Back to Front Again: 1.5 Binary Primitives
ここからどうも ターゲットマシンの影響を受けて教材通りに行かない気がしてきている。TODO