今日、Zコンビネータが動いたので、セーブがてら日記に起こしておこうと思います。こんなのが動くようになりました。例はSICPの問題4.21から取っています。

(( lambda ( n ) (( lambda ( fact ) ( fact fact n )) ( lambda ( ft k ) ( if ( = k 1 ) 1 ( * k ( ft ft ( - k 1 ))))))) 5 ) (( lambda ( n ) (( lambda ( fib ) ( fib fib n )) ( lambda ( fb k ) ( if ( = k 0 ) 0 ( if ( = k 1 ) 1 ( + ( fb fb ( - k 1 )) ( fb fb ( - k 2 )))))))) 5 )

実装の手順を追っていきます。

クロージャを実装

closure manip (7fb7ef7f) · Commits · t / scm-incr · GitLab

(closure label . free-vars) のようなプリミティブを用意し、labelが現すプロシージャに出てくる自由変数の現れをヒープにすべてコピーするようにしています。これはクロージャの実装としては正しくないのですが、set!などの破壊的代入がない限りは多分正しく動きます。rdiがヒープの先頭を指すポインタになっているので、そこからメモリをもらっていきます。

( define ( compile-closure-vars base vars var-env ) ( if ( null? vars ) #t ( begin ( emit "mov rbx, [rsp+(~a)]" ( lookup-environment ( car vars ) var-env )) ( emit "mov [rdi+(~a)], rbx" base ) ( compile-closure-vars ( + wordsize base ) ( cdr vars ) var-env )))) ( define ( compile-closure label vars var-env label-env ) ( emit "mov rax, ~s" ( lookup-environment label label-env )) ( emit "mov [rdi], rax" ) ( emit "mov rax, rdi" ) ( emit "or rax, ~a" closure-tag ) ( let (( bump-size ( * wordsize ( + ( length vars ) 1 )))) ( compile-closure-vars wordsize vars var-env ) ( emit "add rdi, ~a" bump-size )))

先日のコア言語を拡張

extend code form to contain free variable environment (0c83f874) · Commits · t / scm-incr · GitLab

(code args . body) のような、ラベルに紐付いたプロシージャの定義をするのにつかうコア言語を拡張して、 (code args free-vars . body) のような形式を取れるように拡張します。

funcall (a8cd5145) · Commits · t / scm-incr · GitLab

(funcall closure . args) のような形式を追加して、手続きを呼び出せるようにします。クロージャーポインターを１つ決めて、（ここではrsiとする) 呼び出されたクロージャのアドレスを指すようにします。rsiは手続き呼び出しの前後に渡って保存しなければならないので、（そうしないとネストした関数呼び出しでrsiが壊れる)スタックに保存するようにします。自由変数はrsiからのオフセットで参照します。（どの変数がどのオフセットかどうかはコンパイル時に決定できます）

( define ( compile-funcall op args var-env si label-env free-vars-env ) ( let (( args-base ( - si wordsize wordsize )) ( rsp-base ( + si wordsize ))) ( compile-args args var-env args-base label-env free-vars-env ) ( compile-expr op var-env si label-env free-vars-env ) ( emit "sub rax, ~a" #b110 ) ( emit "lea rsp, [rsp+(~a)]" rsp-base ) ( emit "push rsi" ) ( emit "mov rsi, rax" ) ( emit "mov rax, [rax]" ) ( emit "call rax" ) ( emit "pop rdi" ) ( emit "lea rsp, [rsp-(~a)]" rsp-base )))

Schemeの式 -> コア言語のトランスレータを書く

まずはlambdaを、自由変数のリストでアノテートするようにします。例を上げると、 (lambda (x) (+ x y)) => (lambda (x) (y) (+ x y)) のような変換をします。ここらへんのコミットでやっているようです。

annotate lambda (imcomplete) (a57d97cf) · Commits · t / scm-incr · GitLab

free-variables in let* case (d3888547) · Commits · t / scm-incr · GitLab

あとはコア言語に変換していきます。前の記事で書いたような、

( let (( x 5 )) ( lambda ( y ) ( x ) ( lambda () ( x ) ( y ) ( + x y )))) ( labels (( f0 ( code () ( x y ) ( + x y ))) ( f1 ( code ( y ) ( x ) ( closure f0 x y )))) ( let (( x 5 )) ( closure f1 x )))

のような変換です。「今まで定義されたラベルのリスト」と「変換された式」を持ちながら式をトラバースしていくといいのですが、初めはなかなかごちゃごちゃしてうまくかけませんでした。こういう複数の状態を持ちながら何かをするのは多値をうまく使ってやるといいようです。

expr -> labels translator (e775da89) · Commits · t / scm-incr · GitLab

以下の記事を見ながら、マクロ (receive variables mv-expr body...) をうまくネストさせながら処理するとスッキリしました。

https://practical-scheme.net/wiliki/wiliki.cgi?Scheme%3A%E5%A4%9A%E5%80%A4

多値とStateモナドは使いどころが似ているなぁ、などと思ったのですが、どうなんでしょうね？

ここまでやったら、Schemeの式をコア言語に変換した上でコンパイルしてやればよいです。

吐かれるアセンブラ

((lambda (n) ((lambda (fib) (fib fib n)) (lambda (fb k) (if (= k 0) 0 (if (= k 1) 1 (+ (fb fb (- k 1)) (fb fb (- k 2)))))))) 10) をコンパイルしてみましょう。

section .text global scheme_entry f03 : mov rax , [ rsp +(- 8 )] mov [ rsp +(- 32 )], rax mov rax , [ rsi +( 8 )] mov [ rsp +(- 40 )], rax mov rax , [ rsp +(- 8 )] sub rax , 6 lea rsp , [ rsp +(- 8 )] push rsi mov rsi , rax mov rax , [ rax ] call rax pop rdi lea rsp , [ rsp -(- 8 )] ret f14 : mov rax , 0 mov [ rsp +(- 24 )], rax mov rax , [ rsp +(- 16 )] L8 : cmp rax , [ rsp +(- 24 )] jne .L0 mov rax , 159 jmp .L1 .L0 : mov rax , 31 .L1 : cmp rax , 31 je L6 mov rax , 0 jmp L7 L6 : mov rax , 4 mov [ rsp +(- 24 )], rax mov rax , [ rsp +(- 16 )] L11 : cmp rax , [ rsp +(- 24 )] jne .L0 mov rax , 159 jmp .L1 .L0 : mov rax , 31 .L1 : cmp rax , 31 je L9 mov rax , 4 jmp L10 L9 : mov rax , [ rsp +(- 8 )] mov [ rsp +(- 40 )], rax mov rax , 8 mov [ rsp +(- 48 )], rax mov rax , [ rsp +(- 16 )] sub rax , [ rsp +(- 48 )] mov [ rsp +(- 48 )], rax mov rax , [ rsp +(- 8 )] sub rax , 6 lea rsp , [ rsp +(- 16 )] push rsi mov rsi , rax mov rax , [ rax ] call rax pop rdi lea rsp , [ rsp -(- 16 )] mov [ rsp +(- 24 )], rax mov rax , [ rsp +(- 8 )] mov [ rsp +(- 48 )], rax mov rax , 4 mov [ rsp +(- 56 )], rax mov rax , [ rsp +(- 16 )] sub rax , [ rsp +(- 56 )] mov [ rsp +(- 56 )], rax mov rax , [ rsp +(- 8 )] sub rax , 6 lea rsp , [ rsp +(- 24 )] push rsi mov rsi , rax mov rax , [ rax ] call rax pop rdi lea rsp , [ rsp -(- 24 )] add rax , [ rsp +(- 24 )] L10 : L7 : ret f25 : mov rax , f14 mov [ rdi ], rax mov rax , rdi or rax , 6 add rdi , 8 mov [ rsp +(- 32 )], rax mov rax , f03 mov [ rdi ], rax mov rax , rdi or rax , 6 mov rbx , [ rsp +(- 8 )] mov [ rdi +( 8 )], rbx add rdi , 16 sub rax , 6 lea rsp , [ rsp +(- 8 )] push rsi mov rsi , rax mov rax , [ rax ] call rax pop rdi lea rsp , [ rsp -(- 8 )] ret scheme_entry : mov rax , 40 mov [ rsp +(- 24 )], rax mov rax , f25 mov [ rdi ], rax mov rax , rdi or rax , 6 add rdi , 8 sub rax , 6 lea rsp , [ rsp +(0)] push rsi mov rsi , rax mov rax , [ rax ] call rax pop rdi lea rsp , [ rsp -(0)] ret

うまく動いているようでした。

感想

多値返却をうまく使うとプログラムが横に長くなって関数型言語っぽくなることに気づきました。多値は今までよく分からなかったのですが、先週3impを読んでいたら「ああ、継続渡しスタイルで継続を消費するときに複数の引数を渡すことと同じなのか？」などと自分なりの理解ができた気がしたので使ってみました。便利ですね。せっかくなので私がなんとなく多値を理解できたきっかけの例を3impのChapter 2から引用したいと思います。

( define split ( lambda ( pair return ) ( return ( car pair ) ( cadr pair ))))

プログラムを変換してごちゃごちゃやるのは楽しいと思っていたのですが、すこし面倒になってきました。アセンブラのほうが楽...

結局、今回実装したのはクロージャー変換なのでしょうか？結局分からなかった...

次回

次は末尾再帰最適化を実装したいと思います。手続き呼び出しのところのアセンブラをいじっていく感じになるのかな？