let* とスペシャル変数のバグ

http://fixdap.com/p/xyzzy/34388/

(defvar *special* :global)
=> *special*

(let* ((*special* :local)
       (x *special*)) ; ここで *special* がシャドウされてない
  x)
=> :global

関係ありそうなのが

(let* ((VAR VALUE-FORM)) . BODY)

Flet_star は

  • 新しいレキシカル環境 lex_env nlex を作る
  • nlex.let で nlex に (VAR VALUE-FORM) の束縛を追加
    • このとき VALUE-FORM は、eval によって nlex の中で評価される
  • BODY を declare_progn で nlex の中で評価

という流れ。

シンボルは C++ 側では class lsymbol のインスタンスで、その値が2つある。

  • メンバ変数 value == lisp 側で言うと symbol-value に相当
  • レキシカル環境 lex_env に登録された束縛(symbol と値のペアを alist のような形で持ってる)

lex_env::let でレキシカル環境にシンボルの束縛を登録できる。

eval でシンボルを評価すると、スペシャル変数だったらメンバ変数の方を、そうでなければレキシカル環境の値を返す*1

declare_progn はレキシカル環境に登録されたシンボルがスペシャル変数だったら*2

  1. 元の lsymbol::value を保存して
  2. lsymbol::value をレキシカル環境で束縛してる値に変更して
  3. その状態で BODY を評価して
  4. lsymbol::value を元の値に戻す

ということをしているっぽい。

なので、Flet_star から nlet.let したときは単に eval じゃなくて declare_progn 経由で評価させないといけないのではないか。と思うのだけどどうやったら declare_progn 経由にできるのかよくわからない <- 今ココ

余談 #1: このバグはインタープリタで評価した時だけみたい

(funcall
 (compile nil
          (lambda ()
            (let* ((*special* :local)
                  (x *special*))
              x))))
=> :local

余談 #2: こんなバグ見つけた

 *special*
=> :global

(let ((*special* :local-1)
      (*special* :local-2))
  *special*)
;; 2つ目の束縛が無視されてる
=> :local-1

;; さっきの let で変更されてる
*special*
=> :local-2

(setq *special* :global)
=> :global

;; コンパイルすると...
(funcall
 (compile nil
          (lambda ()
            (let ((*special* :local-1)
                  (*special* :local-2))
              *special*))))
;; 2つ目の束縛は無視されてるけど
=> :local-1

;; グローバルの値はだいじょぶ
*special*
=> :global

*1:buffer-local-value もあるのだけど省略

*2:ホントは (declare (special ..) ) の処理もしてるけど省略