sb-c:define-source-transform
の使い方について。
例えば、動的なmodに対しての掛け算mod*
を定義したいとする:
(declaim ((unsigned-byte 32) *modulus*))
(defvar *modulus*)
(defun mod* (&rest args)
(reduce (lambda (x y) (mod (* x y) *modulus*))
args
:initial-value 1))
(ここでは演算の性質は問題にしない。)この mod*
はどんな引数に対してもいちいちreduce
を呼ぶし、mod
や*
も引数の型に特定化されないのであまり効率が良くない。多くの場合、このような問題はインライン宣言で解決する。しかしこの場合については、mod*
をインライン化することでargs
の長さなどがコンパイル時に確定できても、*
とmod
を使ったフォームに展開されたりはしない。SBCLのreduce
には何のコンパイル時変換も定義されていないためだ。こういう時にはdefine-compiler-macro
が使える:
(define-compiler-macro mod* (&rest args)
(if (null args)
1
(reduce (lambda (x y) `(mod (* ,x ,y) *modulus*)) args)))
こうすれば、(mod* a b c)
は(MOD (* (MOD (* A B) *MODULUS*) C) *MODULUS*)
のように展開されることになる。
しかし、コンパイラマクロによる変換には本質的な制約がある: コンパイラマクロはあらゆる定数伝搬の前に展開される。例えば、三乗を求める関数cube
があったとする:
(declaim (inline cube))
(defun cube (operator x)
(funcall operator x x x))
(defun test (x)
(declare (optimize (speed 3)))
(cube #'mod* x))
; disassembly for TEST
; Size: 36 bytes. Origin: #x1007F7EB96
; 96: 840425F8FF1020 TEST AL, [#x2010FFF8] ; no-arg-parsing entry point
; safepoint
; 9D: 488BD0 MOV RDX, RAX
; A0: 488BF8 MOV RDI, RAX
; A3: 488BF0 MOV RSI, RAX
; A6: 488B059BFFFFFF MOV RAX, [RIP-101] ; #<SB-KERNEL:FDEFN MOD*>
; AD: B906000000 MOV ECX, 6
; B2: FF7508 PUSH QWORD PTR [RBP+8]
; B5: FF6009 JMP QWORD PTR [RAX+9]
; B8: CC0F BREAK 15 ; Invalid argument count trap
test
内でcube
がインライン展開されるが、そのインライン展開の中でさらにmod*
が展開されたりはしない。コンパイラマクロが展開されるタイミングを考えれば当然ではある。
さて、このような制約からか、SBCL内部のコード変換にはdefine-compiler-macro
はほとんど使われていない。SBCLの標準の関数の多くは、これに似ているがより強力なsb-c:define-source-transform
で変換されている。[1]
define-source-transformの使い方とコンパイラマクロとの違い
sb-c:define-source-transform
の使い方はdefine-compiler-macro
とほとんど同じである。
;; コンパイラマクロ付きのmod*
(defun mod*-with-compiler-macro (&rest args)
(reduce (lambda (x y) (mod (* x y) *modulus*))
args
:initial-value 1))
(define-compiler-macro mod*-with-compiler-macro (&whole whole &rest args)
(print whole)
(if (null args)
1
(reduce (lambda (x y) `(mod (* ,x ,y) *modulus*)) args)))
;; source-transform付きのmod*
(defun mod*-with-source-transform (&rest args)
(reduce (lambda (x y) (mod (* x y) *modulus*))
args
:initial-value 1))
(sb-c:define-source-transform mod*-with-source-transform (&whole whole &rest args)
(print whole)
(if (null args)
1
(reduce (lambda (x y) `(mod (* ,x ,y) *modulus*)) args)))
(defun test (x)
(declare (optimize (speed 3))
((unsigned-byte 32) x))
(let ((*modulus* 10007))
(funcall #'mod*-with-compiler-macro x x x) ; 1
(funcall #'mod*-with-source-transform x x x) ; 2
(multiple-value-call #'mod*-with-compiler-macro x x x) ; 3
(multiple-value-call #'mod*-with-source-transform x x x) ; 4
(apply #'mod*-with-compiler-macro (list x x x)) ; 5
(apply #'mod*-with-source-transform (list x x x)) ; 6
(cube #'mod*-with-compiler-macro x) ; 7
(cube #'mod*-with-source-transform x) ; 8
))
test
の中には三乗を計算する同等のフォームが8つ並べてある。上のコンパイラマクロとsource-transformにはprint
を付けてあるので、test
をコンパイルすると&whole
引数を束縛しているフォームが次にようにいくつか出力される。(数字が抜けている部分は展開されなかったということである。)
1: (FUNCALL #'MOD*-WITH-COMPILER-MACRO X X X)
2: (#<SB-C::GLOBAL-VAR
:%SOURCE-NAME MOD*-WITH-SOURCE-TRANSFORM
:TYPE #<SB-KERNEL:BUILT-IN-CLASSOID FUNCTION (read-only)>
:DEFINED-TYPE #<SB-KERNEL:FUN-TYPE (FUNCTION * (VALUES T &OPTIONAL))>
:WHERE-FROM :DEFINED
:KIND :GLOBAL-FUNCTION {1003DB5963}>
X X X)
4: (MOD*-WITH-SOURCE-TRANSFORM #:G34 #:G35 #:G36)
6: (MOD*-WITH-SOURCE-TRANSFORM #:G47 #:G48 #:G49)
8: (MOD*-WITH-SOURCE-TRANSFORM #:G78 #:G79 #:G80)
以下、コンパイラマクロとの差異について。
-
コンパイラマクロは定数伝搬の前の展開しかできないが、source-transformは後にも適用される。8を見ればわかるように、インライン展開された
cube
の中のoperator
が#'mod*-with-source-transform
であると推論されたあとに展開が起きている。コンパイラマクロではこのようなことはできない。(とはいえ、この種の最適化が常に完璧に行われるわけではないので過度に期待してはいけない。) -
1と2の違いからわかるように、
(funcall <function> <args>*)
形式のフォームが渡されたとき、コンパイラマクロは&whole
をそのままのフォームに束縛するが、source-transformは(<function> <args>*)
形式に直して束縛する。 -
3-6で示されるように、コンパイラマクロは
multiple-value-call
で呼び出される関数を変換できない。source-transformは(引数の数が決定できれば)変換できる。apply
も同様。[2]
- コンパイラマクロでマクロ展開を放棄したい時は
&whole
を束縛しているフォームをそのまま返せばよい。しかし、source-transformで同様のことをすると、返したフォームに同じsource-transformが無限に適用されてSBCLが落ちてしまう。代わりに、source-transformでは2つ目の返り値でnon-nilを返すことによりこの変換を適用しないことを通知する:
;; 展開を放棄する場合の書き方
(define-compiler-macro foo (&whole form arg) form) ; OK
(sb-c:define-source-transform foo (&whole form arg) form) ; NG
(sb-c:define-source-transform foo (&whole form arg) (values nil t)) ; OK
- source-transformのラムダリストは、元の関数のラムダリストと正確に一致している必要はない。例えば、
mod*
に引数が1つの場合のためのsource-transformを定義しても問題ない:(sb-c:define-source-transform mod* (x) x)
。この場合、source-transformは引数が1つとわかっているケースでのみ呼び出される。この挙動の利用例はSBCLのソースにも見つかるので、想定された使い方だと思われる。ただし、source-transformは(deftransform
と違って)1つの関数につき1つしか定義できないため、複数定義して引数の数でディスパッチするようなことはできない。そのような分岐は1つのsource-transformの中で行う必要がある。例えばgcd
などのsource-transformが参考になる。[3]
-
source-transformは、コンパイラマクロと違ってマクロには定義できない。もっとも、コンパイラマクロをマクロに定義する例はほとんどないようなので、重要ではなさそう。
-
source-transformによる変換は
notinline
で阻止できる。これはコンパイラマクロと同じ。 -
define-source-transform
で定義される展開器は(sb-int:info :function :source-transform <symbol>)
で得ることができて、compiler-macro-function
などと同じように使える。 -
source-transformの展開をREPLで確認したい場合はg000001さんの記事の通りにすれば良い。ただ、記事が書かれた時とはsource-transformの仕様が変わっているようなので(source-transformが必ず
environment
を引数に取るようになっている?)SBCL1.4.14で使える形をメモしておく:
(defun source-transform-expand (form &optional (env (sb-kernel:make-null-lexenv)))
(if (and (consp form)
(symbolp (car form))
(not (special-operator-p (car form))) )
(let ((sb-c::*lexenv* env))
(or (and (fboundp (car form))
(multiple-value-bind (fun win)
(sb-int:info :function :source-transform (car form))
(and win (funcall fun form env))))
(values form T)))
(values form T)))
(source-transform-expand '(apply #'+ (list a b c)))
;; |-> (MULTIPLE-VALUE-CALL #'+ (VALUES-LIST (LIST A B C)))
define-source-transform
(からマクロ展開されたフォーム)はコンパイル時に評価されるわけではない。必要ならeval-when
を使う。