setf の最適化に bug
以下の条件を満たすと place の subforms が評価される順番が変わってしまう。
- PLACE に2つ以上の引数(subforms)
- その中にシンボルがある
- シンボルより後にシンボルではない式がある
3つ目の「シンボルではない式」が2つ目の「シンボル」の値に影響するような副作用を持っていると、本来のものとは違う挙動になってしまう。
再現
ものっそい分かりにくいうえに長くて申し訳ありません。
;; 子 hash-table を保持する親 hash-table (defparameter *table* (make-hash-table)) => *table* ;; key に対応する子 hash-table を返す (defun get-sub-table (key) (or (gethash key *table*) (setf (gethash key *table*) (make-hash-table)))) => get-sub-table ;; (親キー 子キー) に対応する値を返す (defun get-value (key-parent key-child) (gethash key-child (get-sub-table key-parent))) => get-value ;; (親キー 子キー) の値を変更する (defsetf get-value (key-parent key-child) (value) `(setf (gethash ,key-child (get-sub-table ,key-parent)) ,value)) => (setf get-value) ;; ふつーに使う分にはだいじょぶ (setf (get-value 1 2) 12) => 12 (get-value 1 2) => 12 => t ;; place の subform に副作用があると困ったことに (let ((x 2)) (setf (get-value x (incf x)) ; (get-value 2 3) に保存したつもりなのに '(should be stored at (2 3)))) => (should be stored at (2 3)) (get-value 2 3) ;ない => nil => nil (get-value 3 3) ;こんなところに => (should be stored at (2 3)) => t ;; 展開形 (macroexpand-1 '(setf (get-value x (incf x)) :NEW-VALUE)) => (let* ((#:G729 (incf x)) (#:G730 :NEW-VALUE)) (setf (gethash #:G729 (get-sub-table x)) #:G730)) ;; ホントはこうならないといけない ;; (let* ((#:G-1st x) ;; (#:G-2nd (incf x)) ;; (#:G-new :NEW-VALUE)) ;; (setf (gethash #:G-2nd (get-sub-table #:G-1st)) #:G-new))
原因
setf を展開する時に Setf Expansion というものを使って place の subforms を一時変数に束縛する let* 式を作るのだけど、xyzzy の setf は subform が symbol だったときに変数に束縛するんじゃなくて store-form 内の一時変数をその symbol に置き換えるという最適化をしてる(lisp/setf.l の optimize-setf-method)。場合によっては let* で包む必要がなくなる。
;; こんな setf 式を展開したものが (setf (hoge foo (do-bar)) (new-value-form)) ;; Before: foo -> (do-bar) -> (new-value-form) の順に評価される (let ((#:G01 foo) (#:G02 (do-bar)) (#:G03 (new-value-form))) (update-hoge #:G03 #:G01 #:G02)) ;; After: (do-bar) -> (new-value-form) -> foo の順に評価される (let ((#:G02 (do-bar)) (#:G03 (new-value-form))) (update-hoge #:03 foo #:02))
どうしよ
疲れたので思いつきメモだけ。
optimize-setf-method 使わない、というのは置いといて。
optimize-setf-method で vals, stores を前から見ていってるのを逆順にして、かつ副作用があり得る式(constantp でも symbolp でもない式?)があったらそれ以上最適化しないようにすれば大丈夫な気がする。
setf-values で上書きしてしまってるのでどうしよう。と思って見てみたら setf-values で再定義した optimize-setf-method は、newvalues が (values ...) じゃない時に stores を最適化してない事に気づいた。