健全なマクロ展開 - 構文オブジェクト (その5)

その4 までの構文オブジェクトによる展開器は、 Hanson-Bawden の構文クロージャによる展開器を元にしていたため、 いびつなところが残っていました。 Hanson-Bawden で明示リネーミングの対象になるのは構文情報がくっついていないシンボルなのに対して、 構文オブジェクトを使って挿入された識別子の自動認識をおこなうにはすべての識別子には構文情報をくっつけないといけない事情がありました。 このずれから、 局所マクロでリテラルを free-identifier=? で比較するとき、 識別子をいったんシンボルにマクロ展開してから構文情報を再度くっつけていました。 これではリテラルの扱いが明示リネーミングと同じになっています。 とても、 挿入された識別子の自動判別ができているとは言えません。

R. Kent Dybvig 等 Syntactic Abstraction in Scheme 1992 (以下、syntax-case) はこの問題をスマートに解決します。 ここまでは変換世代マークは構文環境で表し、 マクロの定義時構文環境はマクロ変換子に結びつけてきました。 syntax-case では、 変換世代マークは構文環境中の意味束縛と同列になり、 構文環境中に複数のマークが並べることができるようになります。 さらに、 構文環境を個々の識別子に格納することとし、 もはやマクロ変換子に結びつける必要がなくなります。 マクロ変換子が保持する識別子のそれぞれが個別に定義時構文環境を持つようになります。 もちろん、 識別子ごとに構文環境を格納するとはいっても、 構文環境のデータ構造を多くの識別子が共用し、 同じ構文環境のコピーが大量に作られるという意味ではありません。

syntax-case の識別子の興味深い点は、 識別子にくっついている変換世代マークの個数の分、 識別子が多重に意味をもてるようになっていることです。 これにより、 マクロの再帰展開の場合に、 異なる複数の変換世代に渡って、 定義時の一つの意味を共用することが可能になります。 or 構文を簡略にした例でこれがどういうことなのか見てみます。 すべての識別子には同じ環境がくっついています。 この環境には、 環境フレーム E1 と展開前世代マーク M0 が並んでいます。 E1 にはコア特殊形式の意味束縛が入っています。

(letrec-syntax:E1:M0
 ((or:E1:M0 (lambda:E1:M0 (x:E1:M0)
   (cons:E1:M0 (syntax-quote:E1:M0 or:E1:M0) (syntax-cddr:E1:M0 x:E1:M0)))))
 (or:E1:M0 1 x:E1:M0))
E1 ((letrec-syntax:M0 special) (syntax-quote:M0 special) (lambda:M0 special) ...)

ここで、 識別子 letrec-syntax:E1:M0 の意味を環境フレーム E1 から探すには、 マーク以外を削った letrec-syntax:M0 で探します。 すると、 特殊形式の letrec-syntax 構文の意味束縛が見つかります。 letrec-syntax は、 環境フレーム E2 を追加し、 その中に or 構文の意味束縛を定義します。 意未束縛を作るとき、 or:M0 としてマーク以外は取り除きます。 この E2 を追加した環境で or マクロの定義式をマクロ展開します。 そのとき、 例えば識別子 lambda:E2:E1:M0 は lambda:M0 として、 環境フレーム E2、 E1 の順に意味束縛を探し、 lambda 構文の意味を見つけます。 この lambda 構文の展開のために環境フレーム E3 を作り、 x:M0 から x.1 への置換規則を意味として登録します。 syntax-quote 構文は、 識別子を構文情報をくっつけたまま quote 構文に展開します。

(letrec-syntax
 ((or:E1:M0 (lambda (x.1) (cons (quote or:E3:E2:E1:M0) (syntax-cddr x.1)))))
 (or:E2:E1:M0 1 x:E2:E1:M0))
E3 ((x:M0 subst x.1))
E2 ((or:M0 macro <(lambda (x.1) (cons (quote or:E2:E1:M0) (syntax-cddr x.1)))>))
E1 ((letrec-syntax:M0 special) (syntax-quote:M0 special) (lambda:M0 special) ...)

letrec-syntax 構文の本体の展開のため識別子 or:E2:E1:M0 の意味を探すと E2 に or:M0 を見つけます。 新しいマーク M4 を本体にくっつけてマクロ変換子を呼びます。

(letrec-syntax
 ((or:E1:M0 (lambda (x.1) (cons (quote or:E3:E2:E1:M0) (syntax-cddr x.1)))))
 (or:M4:E2:E1:M0 1 x:M4:E2:E1:M0))
E3 ((x:M0 subst x.1))
E2 ((or:M0 macro <(lambda (x.1) (cons (quote or:E2:E1:M0) (syntax-cddr x.1)))>))
E1 ((letrec-syntax:M0 special) (syntax-quote:M0 special) (lambda:M0 special) ...)

マクロ変換子が本体を作り直します。作り直された本体の先頭の識別子からマーク M4 が消えて or:E3:E2:E1:M0 になっているのは、 この識別子はマクロ変換子が保持している or:E3:E2:E1:M0 からコピーしたためです。 一方、 識別子 x:M4:E2:E1:M0 は展開対象式 x 中からコピーしたので、 マーク M4 がくっついています。

(letrec-syntax
 ((or:E1:M0 (lambda (x.1) (cons (quote or:E3:E2:E1:M0) (syntax-cddr x.1)))))
 (or:E3:E2:E1:M0 1 x:M4:E2:E1:M0))
E3 ((x:M0 subst x.1))
E2 ((or:M0 macro <(lambda (x.1) (cons (quote or:E2:E1:M0) (syntax-cddr x.1)))>))
E1 ((letrec-syntax:M0 special) (syntax-quote:M0 special) (lambda:M0 special) ...)

マクロ変換子から戻った直後に、 マクロ変換子を呼ぶ直前にくっつけたのと同じマーク M4 をくっつけます。 このとき、 識別子の環境の先頭に M4 がくっついていると、 M4 を除去します。 これで、 挿入された識別子 or にマーク M4 が付き、 元からある識別子 x からマーク M4 がなくなります。

(letrec-syntax
 ((or:E1:M0 (lambda (x.1) (cons (quote or:E3:E2:E1:M0) (syntax-cddr x.1)))))
 (or:M4:E3:E2:E1:M0 1 x:E2:E1:M0))
E3 ((x:M0 subst x.1))
E2 ((or:M0 macro <(lambda (x.1) (cons (quote or:E2:E1:M0) (syntax-cddr x.1)))>))
E1 ((letrec-syntax:M0 special) (syntax-quote:M0 special) (lambda:M0 special) ...)

本体の展開を繰り返します。 今度は識別子 or:M4:E3:E2:E1:M0 の意味を探します。 まず、 or:M4:M0 を探そうとするのですが、 構文環境の先頭が M4 なので、 M4 を除いて or:M0 を E2 と E1 から探します。 その結果、 or:M0 を E2 で見つけてマクロ展開をしようとします。 上の例では、 このマクロ変換時に syntax-cddr が失敗するのでエラーで完了します。 もしも、 続けて or 構文を再帰展開すると、 or マクロ変換子が保持する or 識別子は常に or:E3:E2:E1:M0 なので、 展開後は or:M5:E3:E2:E1:M0、 次は or:M6:E3:E2:E1:M0 となります。 どの場合でも or:M0 を E2 に見つけて同じ or 構文の意味に辿りつきます。