syntax-rulesとbindingの可視性

たしかにコレ納得が難しい気がする

そろそろsyntax-rulesでも実装するかー、と思ったのだけど、この仕様実装するの面倒すぎる…どうやったらいいんだ

納得する必要が有るのは、多分"R6RS syntax-rulesの展開は1passでは無理"というポイントで、2passでやることを覚悟しながら考えると理解しやすい気がしている。
(実際にはR5RSやR7RSではsyntaxの相互参照が禁止されているをサポートする必要が無いので、やる気になれば1passでできる気がする)
まずはdefine-syntaxではなくdefineだけを使った簡単な例:

(define (bar) (display theNumber)) ;; ★ theNumberはこの行より下で定義されている
(define theNumber 42)

(bar) ;; => 42

この例で、barの定義時にはtheNumberはまだプログラム中に出現していない点に注意する。それでもこのプログラムを正当なプログラムと判断するためには、(display theNumber)を処理する段階でdisplayとtheNumberが構文でなく変数であることを何らかの方法で知っている必要がある。つまり、Schemeコードをコンパイルするためには最初にdefine(や、define-syntax)を収集して、あるシンボルがbindされているのかを見定めなければならない
gistの例に戻ると:

  • "1" が出力されるケース(単純なケース)
(let ()
 (define-syntax foo
   (syntax-rules (LIT) ;; ★ LIT はどこにも定義されない
     ((_ LIT) "1\n")
     ((_ otherwise) "otherwise\n")))
 (display (foo LIT)))

このケースは単純。LITはbindされていないので構文foo内のリテラルとしてマッチする。

  • "1" が出力されるケース(bindされたケース)
(let ()
 (define-syntax foo
   (syntax-rules (LIT) ;; ★ ここのLITは直下でdefineされるLITの意
     ((_ LIT) "1\n")
     ((_ otherwise) "otherwise\n")))
 (define LIT 3)
 (display (foo LIT)))

このケースは最初のbarの例に近い。コンパイラ最初にプログラムをスキャンしてfooが構文、LITが変数であることを知ってから、syntax-rulesを処理する必要がある。

  • "otherwise"が出力されるケース
(let ()
 (define-syntax foo
   (syntax-rules (LIT) ;; ★ ここのLITは最初のケース同様どこにもdefine(-syntax)されていない
     ((_ LIT) "1\n")
     ((_ otherwise) "otherwise\n")))
 (let ((LIT 3)) ;; ★ ここのLITは別物
  (display (foo LIT))))

この場合は、letでbindされるLITと、syntax-rulesのリテラルリストにあるLITは別物ということになる。
環境の内容は特にdefine(-syntax)で定義されるものである必要はなく、let/letrecでも同様のことが言える。

(let ((LIT (foo LIT)))
 (display LIT))      ;; => 1

(letrec ((LIT (foo LIT)))
 (display LIT))      ;; => otherwise

letの場合は、(foo LIT)を処理する環境ではLITはbindされていないため、1が出力される。letrecの場合は、(foo LIT)を処理する環境にはLITがbindされているため、otherwiseが出力されることになる。(このとき、letrecケースもエラーや未定義にはならないことに注意する - 構文fooを処理する際にLITの内容そのものは使用しないため、bindされたか否かだけが影響している。)
つまり、syntax-rulesのリテラルリストは:

  • syntax-rulesを定義した環境(= だいたいの場合はlet-syntax/letrec-syntax/define-syntaxの書かれた環境)の内容にのみ影響される
  • syntax-rulesを定義した環境で、リテラルリストに書かれているidentifierがbindされていたら、使用時に同じものにbindされているならマッチする
  • syntax-rulesを定義した環境で、リテラルリストに書かれているidentifierがbindされていないなら、使用時に字面上の名前が一致し、かつ、bindされていないならばマッチする

この性質で大きく問題になるのはR6RS/R7RSで補助構文をrenameした場合で、例えば、elseのようなidentifierをrenameした場合の挙動がかなりマチマチになっている。

(import (except (scheme base) else)
        (scheme write)
        (rename (scheme base) (else my-else)))

(case #f
      (my-else (display "Oh!\n")))

のようにして、elseをrenameしたときの挙動をyunibaseの収録scheme処理系で調べてみると:

root@5895067554f4:/yunibuild# yunified/chez-scheme /import/check3.scm
Oh!
root@5895067554f4:/yunibuild# yunified/sagittarius /import/check3.scm
Oh!
root@5895067554f4:/yunibuild# yunified/nmosh /import/check3.scm
Oh!
root@5895067554f4:/yunibuild# yunified/chibi-scheme /import/check3.scm
Oh!

のようにR6RSの殆んどの処理系とchibi-schemeは期待通りelseをmy-elseにrenameできているが、Gauche、Guile、Chickenはrenameできていない(実行するとエラーになる - 課題: エラーにならないような判定プログラムを記述できるか)。
elseをrenameできない処理系ではelseはbindされておらず、字面上の名前が一致しないとelseとして使用できないということになる。