笑ってしまったのは、明らかに意図していない動作をするコードであることでR6RSテストスイートがパスできること。バグの上に成り立つコードなんてイヤだ。
ということで何が問題かを洗い出し、対策を考えることにする。
問題とするコードはとりあえず以下のものだけに絞る。
(define-syntax loop
(lambda (x)
(syntax-case x ()
((k e ...)
(with-syntax ((break (datum->syntax #'k 'break))
#'(call-with-current-continuation
(lambda (break)
(let f () e ... (f)))))))))
(let ((n 3) (ls '()))
(loop
(if (= n 0) (break ls))
(set! ls (cons 'a ls))
(set! n (- n 1))))
これはR6RSの仕様書にもあるキーワードを追加しないloopマクロの実装。現状ではbreakは存在しないといわれて怒られる。(でもテストケースでは通る)loopは以下のように展開されることがコードから読み取れる。
(let ((n 3) (ls '()))
(call-with-current-continuation
(lambda (break) ;; *1
(let f ()
(if (= n 0) (break ls)) ;; *2
(set! ls (cons 'a ls))
(set! n (- n 1))
(f)))))
ここで問題になるのは、*1のbreakと*2のbreakが同じ識別子にならないこと。現状の実装ではマクロ内にあるパターン変数と実際に展開されるコード内にあるシンボル(識別子)が同じと考えられそうなら同じものを使うということをやっている。正直それが正しいかどうかは分からないが、マクロ展開器は(特にこのパターンでは)どの識別子がローカルに束縛されているのか知ることが出来ない。これがネックになって上記のようなことを行っている。
まぁ、それはおおむね上手くいっている感じなのでそれを突き詰めるようにしていこうと思う。
上記の例だけ見れば、パターン変数としてのbreakはマクロ作成時に取得せざるを得ない。このとき取得したパターン変数含む環境をマクロ環境とする。マクロ展開時にはこのマクロ環境からbreakパターン変数を探すことになる。
現状で何が問題か?なぜか見つかったパターン変数が識別子ではなくシンボルになっていた。実際テンプレートないでbreakが見つかると、with-syntaxで作ったbreakがパターン変数として認識されるため置き換えが発生する。その際になぜか置き換えられたパターン変数が識別子ではなくシンボルであった。
考えられそうなこととして、with-syntaxは以下のように展開される。
(syntax-case x ()
((k e ...)
(syntax-case (list (datum->syntax #'k 'break)) ()
((break)
(let ()
#'(call-with-current-continuation
(lambda (break)
(let f () e ... (f)))))))))
この際にパターンに与えられるS式からシンタックス情報が剥ぎ取られている可能性がある。この問題の原因が分かった。datum->syntaxがシンボルを識別子にしていないんだ。なんらかの理由があって識別子のライブラリとマクロ環境(もしくは現在の環境)が保持しているライブラリが一緒だった場合ラップしないようにしてたんだ。なんでだったかな?
No comments:
Post a Comment