Gauche:#<syntax>や#<macro>の評価

Gauche:#<syntax>や#<macro>の評価

Gauche:WishListより移動

eval内で、macroやspecial formの実体を、macroやspecial formとして扱ってほしい

nekoie(2008/02/10 19:49:45 PST): ややっこしいのですが、やりたい事は、

(let ((lambda-entity lambda))
  ((eval `(,lambda-entity (arg) arg) (interaction-environment)) 123))
=> 123

のように、evalに、macroやspecial formの「実体」を渡しても機能してほしいです。 (guileはこの挙動をします。mzschemeではそもそもmacro等の実体にさわった時点でエラー。) (Gaucheでは、lambdaの実体がspecial formとして認識されず、(arg)が先に評価され、 エラーになります。)

Gaucheでは、「if」等のspecial formやmacroも、 「if自体はシンボルで、それ自体を(括弧つけずに)評価すると、何らかの実体が得られ」ます。 この「実体」は、普通にSchemeコードを書いている時にはほぼ使われないようです。 というのは、コードとして書かれたS式は、 src/compile.scm(gauche.internalモジュール)のcompile手続きによって、一旦 内部表現に変換されてから実行されるのですが、その際に、 マクロは展開されて消滅し、special formは引数と共に専用のインストラクションコードに 展開されるからです(多分)。

この内部表現への変換は、マクロ展開時と同じタイミングで行われているので、

(let ((if2 if))
  (if2 #t 'ok (print "not printed")))

のような事はできません("not printed"がprintされた後にエラーになる)。

しかし、evalでは、またS式→内部表現への変換のフェーズから行われるので、

(let ((if2 if))
  (eval `(,if2 #t 'ok (print "not printed")) (interaction-environment)))

が機能しても問題ない気がしますし、実際にguileでは機能します。 これはeval内でのみの挙動(というか、compile手続きによる挙動)なので、

(for-each (lambda (z) (z #t 'ok (destroy-the-world))) `(,if))

のような時にはこれまで通り、エラーとしていいと思います。

http://d.hatena.ne.jp/ranekov/20080211 にもメモがあります。

とりあえずパッチを書きました。

--- src/compile.scm.orig        2008-02-11 06:26:18.000000000 +0900
+++ src/compile.scm     2008-02-11 07:33:22.000000000 +0900
@@ -1365,7 +1365,10 @@
            (module-qualified-variable? head cenv)
            (let1 mod (ensure-module (cadr head) 'with-module #f)
              (cenv-lookup (cenv-swap-module cenv mod)
-                          (caddr head) SYNTAX)))))
+                          (caddr head) SYNTAX)))
+      (and (macro? head) head)
+      (and (eq? (class-of head) <syntax>) head)
+      ))

 ;; pass1 :: Sexpr, Cenv -> IForm
 ;;
@@ -1432,8 +1435,10 @@
               (pass1/global-call head))
              ((lvar? head)
               (pass1/call program ($lref head) (cdr program) cenv))
-             ((macro? head) ;; local macro
+             ((macro? head) ;; local macro / macro entity
               (pass1 (call-macro-expander head program (cenv-frames cenv)) cenv))
+             ((eq? (class-of head) <syntax>) ;; syntax entity
+              (call-syntax-handler head program cenv))
              (else
               (error "[internal] unknown resolution of head:" head)))))
       (else

nekoie(2008/02/11 02:11:35 PST): ヤバい。test codeを、

(for-each (lambda (z) (z #t (write "ok") (write "ng"))) `(,if))

のようにしたら、guileで"ng"出力無しに、正常に動いてしまった。

先に内部表現にコンパイルしてしまうGaucheでは、この挙動に変更するのは多分、大改造しないと無理そうな気がする(そもそもこの挙動へと変更する必要があるのかどうかは置いておいて)。

このguileの挙動から考えれば、evalで実体を渡せば動くのも当然だったようです。

この部分(syntaxやmacroの実体)は下手に手を入れると、泥沼にはまりそうな気がしてきました。


この問題が厄介なわけ

Shiro(2008/02/26 07:33:10 PST): 返事が遅くなりましたが、これはマクロの根本に 関わる非常に興味深い問題でして、なかなかまとめる時間が取れません。

とりあえず答えだけ言っておくと、解決法はたぶんふたつあって、 first class macroを認めるかhygienic macro(の拡張)を使うかということになります。 Scheme的な解決法は後者です。ただし現在のGaucheでは必要な手続きが 一般には見えるようになっていません。無理やりやるならこんな感じになります。

  (let ((lambda. ((with-module gauche.internal global-id) 'lambda)))
    ((eval `(,lambda. (arg) arg) (interaction-environment)) 123))

あいにく、global-idはcompile.scmの内部手続きなので将来に渡って使える保証は ありません。将来的にはたぶんこう書けるようにすると思います。

  (let ((lambda. (syntax lambda)))
    ((eval `(,lambda. (arg) arg) (interaction-environment)) 123))

((with-module gauche.internal global-id) 'lambda) で返される値は #<identifier gauche#lambda> ですが、これはgaucheモジュール中のlambdaを 示す識別子です。lambdaというシンボル自体は環境によってその意味を変えてしまいます (たとえば当該モジュール中でlambdaというグローバル変数が定義されてたり、 lambdaというローカル変数が定義されてたりする場合)。 しかし #<identifier gauche#lambda> はどこに持っていってもgaucheモジュール 中で定義が与えられているlambda、を示しているので、gaucheモジュール中のlambdaを 破壊的に変更しない限りは意味が変わりません。

なぜ#<syntax lambda>という「値」ではだめかというと、Schemeの意味論では 変数と変数の値との結びつきが実行時に生じるものであるのに対し、 構文キーワードと構文の意味との結びつきは実行に先立って判明していなければ ならないからです (他の多くの言語もそうです。ただ、そうでない言語もあります)。

(あとは時間が出来たら書くかもしれない)。

Gaucheにおける#<syntax ...>の意味

Shiro(2008/02/26 23:20:55 PST): ああ、#<syntax ...> という表記が紛らわしいのかもしれません。

(syntax expr) が返すべきsyntactic objectとは、実装側から見れば式とその式を 解釈するコンパイル時環境とをセットにしたものです。特に (syntax <symbol>) は <identifier> を返し、これはそのシンボルがプログラム中でどのように束縛されているか (e.g. 標準Scheme環境のグローバル束縛を参照しているのか、ローカルに定義された 変数の束縛を参照しているのか、はたまたマクロ展開によって挿入されたのか、など) を保持しているオブジェクトです。

一方、Gaucheにおける#<syntax ...> というのはコンパイラがその構文を解釈する時に呼び出すハンドラのようなものです。 本来、それはコンパイル時にさえ存在していれば良く、実行時には意味を持たないもの のはずなのですが、Gaucheではコンパイルと実行がインターリーブするのと 簡素化のために、グローバル環境にコンパイル時束縛と実行時の値の束縛とをいっしょくたに して放り込んでいます。ifを評価して#<syntax if>が帰ってくるのはその 予期せぬ副作用であると思ってください。

Scheme:マクロ:CommonLispとの比較でちょっと出しましたが、Schemeの 意味論ではS式はまずマクロ展開のために情報を保持したsyntactic objectに変換され、 そこでマクロ展開が行われてからコンパイルされて実行可能なオブジェクトになります。 (ここでの「executable」はそのScheme処理系が内部的につかう何らかの実行形式という ことで、OSが実行可能なバイナリファイルとは限りません。実装によってはGaucheのように VMインストラクション列であったり、あるいは単なるツリー構造かもしれません。 また、ここでは"parse"と書きましたがこのフェーズにはっきりした名前は規格上は 与えられてないと思います。)

      parse                      compile              execute
 S式 --------> syntactic object --------> executable ---------> 値
   

で、構文キーワードやマクロなどはcompileの段階で意味が確定する必要があります。 これは、「コンパイル時環境」でsyntactic objectに対応する構文やマクロの定義を 引っ張ってくることで実現されます。例えばローカルにシャドウされていないifが出現 した場合、こんな感じで:

      parse                            コンパイル時環境
 if --------> #<identifier gauche#if> -----------------> #<syntax if>

コンパイラはそのifがGaucheで定義されているifの意味であることを知り、 #<syntax if>が保持しているハンドラを使ってコンパイルを行います。

一方、ローカルにシャドウされていないcarが出現した場合:

      parse                               実行時環境
 car -------> #<identifier gauche#car> ----------------> #<subr car>

#<identifier gauche#car> にはコンパイル時に特に意味が与えられて いないのでコンパイラはスルーして、それを直接executableに埋め込みます。 そしてそのコードが走る時に、実行時環境からcarに対応する値が引っ張られて、 #<subr car>が得られます。

define-syntax, let-syntax, letrec-syntaxはコンパイル時環境を変更する操作で、 defineやletは実行時環境を変更する操作です。 従って概念的には、両者は全く別の世界に存在するものと考えた方がすっきりするでしょう。 Gaucheで(define if2 if)が可能なのはグローバル実行時環境とグローバルコンパイル時環境 が実装の都合で重なっていることの副作用で、ifが構文だからdefine-syntaxのabbreviation と解釈している、というわけではないです。

将来的に、コンパイル時環境の移し替えを実現するために (define-syntax if2 if) というのを許すかもしれませんが、 (define if2 if) は意味的におかしいのでいずれはフェードアウトすると思います。

More ...