macro

まだクリスマスのお菓子を賞味中ですよ。

Chezのcall/cc論文の紹介(のようなもの)

実装が大変なschemeの機能の紹介

lem上で動く横スクロールシューティングゲームを作った と、lemの作者さんが宣伝されてました。お堅いeditor言えども、息抜きは必要です。emacsに負けないようにゲーム開発お願いします。

いまから始めるCommon Lisp ですって。CL界の色々を知らないのでためになるなあ。真面目に勉強しようと過去に買った本を見てる。んだけど、 FORMAT手習いとか 黒帯の為のLOOPなんて章があって、身まがえちゃうぞ。 難しいから、心して掛かれと言ってますもん。

難しいのは他にも有った。マクロ。マクロを題材にした本を2冊も持ってる。しかもそれがCL上に搭載された物って、どんだけ難行苦行になるのやら。

簡単なマクロの紹介

もっとカジュアルにいかないかね。って事で。。。

example of macro

前回genstubをsnowからgauche用のスクリプトに変換する際、下記のような使い方が有った。

[sakae@fb /tmp]$ cat test.scm
      (println "/* Generated by genstub.  Do not edit. */")
      (println "/* source: ~a */" file)

このprintlnは有りそうでgaucheには無い手続きだ。実行するとエラーになる。

*** ERROR: unbound variable: println
    While loading "./test.scm" at line 1
Stack Trace:
_______________________________________
  0  (println "/* Generated by genstub.  Do not edit. */")
        at "./test.scm":1

その時は先を急いでいたので、emacsの M-% (query-replace)コマンドを使って

Query replace: println
Query replace println with: format #t

のように変換してしまった。まあ、一回しか動かさないスクリプトなんで、こういう方法でもいいんだけど、何となくschemerっぽくないと思うんだ。

schemerは、無い手続きをその場でさっと書いて、schemeに変換をやらせちゃうのが性だ。 オイラーもやってみる。

(define-syntax println
  (syntax-rules ()
    [(_ constr arg ...) (begin
                          (format #t constr arg ... )
                          (format #t "~%"))]))

;; test for println
(println "/* Generated by genstub.  Do not edit. */")
(define file "mytest.scm")
(println "/* source: ~a */" file)

どうせならschemeに特有な 衛生的マクロと言うのを使ってね。元のprintlnは使い方が2種ある。引数が一つの場合と、複数ある場合ね。

鍵カッコ内の左がパターン、右側は展開系。パターンに一致したら、右側の展開系に展開される。パターンの最左は _ になってるけど、これは通例かな。この場合はprintlnと書いた方が分かり易いか。そうするとargの後に書かれている ... (ドット3個は伊達じゃ無い)は、argの0回以上の繰り返しを意味する。

マッチしたら、右側で展開。左側のarg等が変数と見做されて、そこに埋め込まれる。今回は、処理した後に改行を行うように2番目のformatで指示しておいた。 (前回はこれを怠っていたため、変換したコードが最密充填になってたのさ)

gosh> ,l test.scm
/* Generated by genstub.  Do not edit. */
/* source: mytest.scm */
#t

書いたマクロが正しく展開されたか、確認する方法が用意されてる。

gosh> (trace-macro 'println)
(println)
gosh> (println "hello")
Macro input>>>
(println "hello")

Macro output<<<
(begin (format #t "hello") (format #t "~%"))

hello
#<undef>
gosh> (println "aaa ~a bbb ~a" "AAA" "BBB")
Macro input>>>
(println "aaa ~a bbb ~a" "AAA" "BBB")

Macro output<<<
(begin (format #t "aaa ~a bbb ~a" "AAA" "BBB") (format #t "~%"))

aaa AAA bbb BBB
#<undef>

展開対象のマクロを登録する(登録取り消しは、untrace-macro)。後は、普通に実行。Macro outputが、実際の実行時に展開され、評価される。簡単に使える事が分かったので、積極的に利用してみよう。

マクロは普通、新しい構文を作るのに使われるけど、今回は、元ファイルをなるべく現存させようと言う意図でつかってみた。

衛生的/伝統的

gaucheの説明書では、上で使ったdefine-syntax式のマクロを衛生的と称し、元からあるdefine-macroってのを、尊敬を込めて伝統的マクロって説明してる。

衛生的マクロの事を人によっては健全なマクロとも言ったりしてる。健全の対語は不健全だな。 衛生的な反語は不潔だな。

と、なると、伝統的ってのは、上の対比で言うと、不潔とも不健全とも取れる訳だ。みんな大人の対応してるんだね。(個人的な感想です。無視してください)

で、衛生的なやつと伝統的なやつがどんな割合になってるか、えいやーーと調べてみる。

(base) [sakae@c8 Gauche]$ find . -name '*.scm' | xargs grep define-macro | wc
    204    1054   13912
(base) [sakae@c8 Gauche]$ find . -name '*.scm' | xargs grep define-syntax | wc
    489    2192   32192
(base) [sakae@c8 Gauche]$ find . -name '*.scm' | xargs grep let-syntax | wc
     81     490    5699
(base) [sakae@c8 Gauche]$ find . -name '*.scm' | xargs grep letrec-syntax | wc
     34     618    5872

思った通り、健全な系統が(積極的に)使われる傾向にあるね。でも、昔Lisp派の人が言ってなかたっけ? 時には、不健全を承知で、それを制御しながら使うんだと。

アナフォリックマクロ

日常言語では,アナフォラ(前方照応,anaphora)とは会話の中で前に出てきたことを指す
表現のことだ. 英語で一番馴染み深いアナフォラは恐らく「それ」で,
「レンチを取ったら,それを机に置いてくれ」等と使われる

オイラーは、それの他に、あれだよ、あれ って発言が段々と多くなってきてる。健忘症の 初期症状なんですかね? 老人性健忘症だから、しょーがない。

#?=

これが何を意味するか、さーと閃く人は、schemerならぬgaucherである。スキーマーって 言うより、ガッチャーの方が個性的でいいね! 子供に受けそう。

debugの必需品。print文を簡単に埋め込めるやつだ。

gosh> (define (hoge x y)(+ x y))
hoge
gosh> #?=(hoge 3 4)
#?="(standard input)":1:(hoge 3 4)
#?-    7
7

余り良い例ではないけど、評価前のS式と結果を簡単に確認出来る。関数の深い所にあるS式を確認したい時に便利なやつだ。

どんな仕組みになってるのかな? きっと、マクロになってるだろう。

define-syntax #?= とかのキーワードで 探してみるも、さっぱりヒットしない。ちゃんと使えているんだから、何処かに定義があるはず。親切な説明書に当たります。

gosh> #?,  (hoge 3 4)
#?,"(standard input)":1:calling `hoge' with args:
#?,> 3
#?,> 4
#?-    7
7

こういうのも有るんだ。古典本には無かった機能だ。説明書は最新のものに当たるべきだね。

で、大事な解説がさらっと書いてあった。

Gaucheのリーダには、中間の結果を出力するために、 #?で始まるいくつかの構文が
用意されています。

(1)目的の式にデバッグスタブを付加するのに、 式全体を余分な括弧でくくらなくて
   良いのですぐできる
(2)デバッグスタブをエディタで探したり取り除いたりするのが極めて簡単

注目は、Gaucheのリーダーって所。(1)を実現するためには、S式の縛りから逃れる必要がある。俗に言うリーダーマクロってやつだな。

リードマクロ

CommonLispでは、この機能をユーザーに解放してるけど、gaucheの場合(Scheme全般でも)、作者に委ねる事になってる。#?の他にも、#で始まる、#構文って説明が基本的な構文の最初の所に出てたんで、見ておくとよい。

と言う事で、#?=を読んだ瞬間に、lib/gauche/vm/debugger.scm が参照されて、マクロのお出ましになる。

;; Debug print stub ------------------------------------------
;; (this is temporary implementation)
(define-syntax debug-print
  (syntax-rules ()
    [(_ ?form)
     (begin
       (debug-print-pre '?form)
       (receive vals ?form
         (debug-print-post vals)))]))

temporaryって事は、使用者のアイデアで変更されるかもって事かな。表の説明書にも、アイデア絶賛募集中って有ったから。

うん、これならわかるよ。実行前に?formに何かする(多分、表示)。次に?formを評価する。多値が返って来る可能性が有るんで、receiveを使って結果をvalsに受け取る。そして、post-itで、表示するんだろうね。へへへ、さりげなくアナフォリックを使ってみた。ボケていないよね。

秘密の花園

なんじゃい、ハーレークィンみたいなタイトル。間違って飛んで来た人御免。ここはそういう場所じゃないから、さっさと退散してね。

上で、リーダーマクロは、インプリメンターの特権と書いた。その場所を特定したい。そして、あわよくば、勝手に使ってみたいものだ、と言う見え見えな下心です。その為には、場所の特定が必要って訳。

多分、ソース読み込んでいるあたり、read.cとかにありそうなんだけど、地道に足でかせいでみる。手掛かりは、debug-printしか有りませんで、でか長さん、ってまた刑事物風。

debian:Gauche$ find . -name '*.scm' | xargs grep debug-print
./ext/peg/peg.scm:              name (debug-print-width) s)
./ext/peg/peg.scm:        (debug-print-post (list r v s))
./src/builtin-syms.scm:    (debug-print               SCM_SYM_DEBUG_PRINT)
./src/autoloads.scm:          (:macro debug-print debug-funcall)
  :

debug-printってscheme世界のシンボル。それがダーと出て来るのは頷ける事だけど、それに混じって何やらC語世界の名前が引っかかってきた。怪しそうだから、今度はそいつを洗ってみろ。

debian:Gauche$ find . -name '*.c' | xargs grep SCM_SYM_DEBUG_PRINT
./src/read.c:                    return SCM_LIST2(SCM_SYM_DEBUG_PRINT, form);

デカ長さん、やつ(itって隠語が相応しい)のヤサが割れました。よーし、踏み込んでみろ。

            case '?': {
                /* #? - debug directives */
                reject_in_r7(port, ctx, "#?");
                int c2 = Scm_GetcUnsafe(port);
                switch (c2) {
                case '=': {
                    /* #?=form - debug print */
                    ScmObj form = read_item(port, ctx);
                    return SCM_LIST2(SCM_SYM_DEBUG_PRINT, form);
                }
                case ',': {
                    /* #?,form - debug funcall */
                    ScmObj form = read_item(port, ctx);
                    return SCM_LIST2(SCM_SYM_DEBUG_FUNCALL, form);
                }
                case EOF:
                    return SCM_EOF;
                default:
                    Scm_ReadError(port, "unsupported #?-syntax: #?%C", c2);
                    return SCM_UNDEFINED; /* dummy */
                }

よし、割れたな。ちょいとちょっかいを出してみるか。

gosh> #?@ (hoge 3 4)
*** READ-ERROR: Read error at "(standard input)":line 1: unsupported #?-syntax: #?@
Stack Trace:
_______________________________________
  0  (read)
        at "/usr/local/share/gauche-0.97/0.9.9/lib/gauche/interactive.scm":236
gosh> 7

ちゃんとガードしてて、付け入る隙がありませんぜ!!

しゃーないな、上部組織はどうなってる? あわよくば、付け入って乗っ取りを企め。そう、サツとヤーさんは紙一重の違いしかありませんです。

    case '(':
        return read_list(port, ')', ctx);
    case '"':
        return read_string(port, FALSE, ctx);
    case '#':
        {
            int c1 = Scm_GetcUnsafe(port);
            switch (c1) {
            case EOF:
             :
            case '?': {
                /* #? - debug directives */
                reject_in_r7(port, ctx, "#?");
                int c2 = Scm_GetcUnsafe(port);

ってな具合に、しっかりと根を下ろしてます。鉄壁の構造(hard code)です。もっと組織化した構造(手続きに登録するだけで、機能するみたいな)になってるかと思ったら、そうはなっていない。残念至極であります。

まあ、こうしておかないと、オイラーみたいな不届き者が付け入る隙を与えてしまいますからねぇ。これでいいんです。

at sbcl

なら、CommonCLではどうか? ちょいと探してみるか。

debian:sbcl-1.5.7$ find . -name 'read*'
./src/code/reader.lisp
./src/code/readtable.lisp
./obj/from-xc/src/code/reader.lisp-obj
./obj/from-xc/src/code/readtable.lisp-obj
./obj/from-host/src/code/readtable.fasl
./obj/asdf-cache/sb-posix/test-output/read-test.txt
./tests/reader.pure.lisp
./tests/reader.impure.lisp

C語のファイルが引っかかってこないのは、オイラーの大いなる誤解なのかな? 取り合えず、./src/code/reader.lisp とかを見る。

(defun %make-dispatch-macro-char (dtable)
  (lambda (stream char)
    (declare (ignore char))
    (read-dispatch-char stream dtable)))

こんな、内部関数で、登録するのね。で、このファイルの中にreadも定義されてた。

(defun read (&optional (stream *standard-input*)
                       (eof-error-p t)
                       (eof-value nil)
                       (recursive-p nil))
  "Read the next Lisp value from STREAM, and return it."

受け付けはSTREAMってなってる。これならUnixの流儀に従って、デフォは標準入力だけど、ファイルディスクリプタを渡せば、ファイルからの読み取りになる。 後の解釈は、lispの関数レベルで好きにしてくれって事だな。

readがlisp語で書かれているって事が大事。万能read関数を簡単に実現出来る。お望みとあらば、最近 gone awayして話題をさらっている国外逃亡者も水際で発見出来たでしょう。何、写真を読み込んで写ってるものから読み出すだけですから。(read :face)とかするだけでOK。後は、それをfilterすれば宜しい。こういうのは、もうサツが導入してるよね。ある意味恐い。

;; Install the (easy) standard macro-chars into *READTABLE*.
(defun !cold-init-standard-readtable ()
  :
  ;; Easy macro-character definitions are in this source file.
  (set-macro-character #\" #'read-string)
  (set-macro-character #\' #'read-quote)
  ;; Using symbols makes these traceable and redefineable with ease,
  ;; as well as avoids a forward-referenced function (from "backq")
  (set-macro-character #\( 'read-list)
  (set-macro-character #\) 'read-right-paren)
  (set-macro-character #\; #'read-comment)

普段何気なく使ってる文字も内部的には、マクロ文字扱いなのか。すっ飛んだ仕組みだ事。こうなっているからこそ、自由度256%(当社比)を実現出来ているんだな。 すんばらしいと言うか、おっとろしいな。

これらのlisp語を駆動するのは、src/runtime/*.c に置いてあるのか。bsdとかarmとかx86-xxとか色々なやつに対応出来るようになってる。riscvなんていう新式の石にも対応。

折角足を延ばしたんでコードを見ると。main.cと言うラッパーが有って、本筋はruntime.cにあるsbcl_mainが担当してる。ざっと見

    initial_function = load_core_file(core, embedded_core_offset,
                                      merge_core_pages);
      :
    create_initial_thread(initial_function);

これが大事な仕事なのね。 それにしても、各種OSを集大成してるmainは、#if definedの嵐かと思ったら(emacsの裏側のC語が怖い)そうでもなかった。

runtime.c:41:#if defined(SVR4) || defined(__linux__)
runtime.c:371:#if defined(LISP_FEATURE_WIN32) && defined(LISP_FEATURE_SB_THREAD)
runtime.c:454:#if defined(LISP_FEATURE_SB_LDB)
runtime.c:468:#if defined(LISP_FEATURE_WIN32) && defined(LISP_FEATURE_SB_THREAD)
runtime.c:726:#if defined(SVR4) || defined(__linux__) || defined(__NetBSD__) || defined(__HAIKU__)

#?= の実使用から

再びdebug-printです。その名の通り、ちょいと動作を知りたい所に埋め込んで、簡単に確認が出来る。前回やったgenstubに埋め込み。

(define-method emit-definition ((cproc <cproc>))
  (format #t "static void ~a(ScmObj *SCM_FP, int SCM_ARGCNT, void *data_)"
           #?=(c-name-of cproc))
  (format #t "{")
    :
gosh> ,l ./genstub
  :
#?="./genstub":168:(c-name-of cproc)
#?-    "stdlib_vm_dump"
#?="./genstub":168:(c-name-of cproc)
#?-    "stdlib_vm_trace_stack"
#?="./genstub":168:(c-name-of cproc)
#?-    "stdlib_vm_instructionP"
#?="./genstub":168:(c-name-of cproc)
#?-    "stdlib_closure_code"
#?="./genstub":168:(c-name-of cproc)
#?-    "stdlib_procedure_info"
#t

オイラーの感覚では、debugの為のstubと言うより、オシロスコープのプローブだな。見たい所に針を当てれば、結果がダーと表示される。関数と言う部品の中であろうとお構いなし。

slibからtraceを引っ張って来て使うってのを、よくネット上に記事として見かけるけど、この#?=の方が、ずっと強力じゃん。

更に、ちょいとイリーガルになるかも知れないけど、庶民の知恵。

(c-name-of ...) を、(ZZc-name-of ...) に変更してから実行してみる。

gosh> ,l ./genstub
#?="./genstub":168:(ZZc-name-of cproc)
*** ERROR: unbound variable: ZZc-name-of
    While loading "./genstub" at line 367
Stack Trace:
_______________________________________
  0  (ZZc-name-of cproc)
        at "./genstub":168
  1  (ZZc-name-of cproc)
        at "./genstub":168
  2  (format #t "static void ~a(ScmObj *SCM_FP, int SCM_ARGCNT, vo ...
        at "./genstub":167
  3  (emit-definition cproc)
        at "./genstub":69
  4  (process-define-cproc form)
        at "./genstub":363
  5  (with-input-from-file file (lambda () (let loop ((form (read) ...
        at "./genstub":358
  6  (eval expr env)
        at "/usr/local/share/gauche-0.97/0.9.9/lib/gauche/interactive.scm":269

損して得取れってやつ。バックトレースが取れた。但し、不格好だけど。これ、未知のコードを追いかける時に便利です。

そこで、お願いです。

(1) 上記のbacktraceまがいの事を リーダーマクロで提供して欲しい。
(2) #?= の結果をログに落とせるようにして欲しい。

(1)は、贅沢病かも知れませんが、後で消す時に楽です。(2)は、define-syntaxの追加ですかね。新なリーダーマクロを定義するってのは、ミニマム主義のschemeにはそぐわないですから。

(2)の必要性ですが、本例ではスクリプトの出力がファイルに行くのでrepl画面が綺麗なままです。出力がreplに出てくるようなものだと、debug-printと混じってしまって悲惨な状態になっちゃう。出力が大量にあると尚更です。よって、debug-printの結果だけログファイルに落とせれば好都合。 ファイルに残っていれば、後の加工なり閲覧は簡単です。

他にちらっと頭を過った事として、 emacs上のrepl画面をファイルにするってのが妥当? vimな人は? 黒い端末からreplを開いている人は? こう考えてみると、ログに落とすのが妥当なような気がする。でも、ログしてるのを忘れていて、ログが肥大化するって問題が有るな。うーん、難しい。

もう少し考えてみよう。to be continue.

散歩中に閃いた。goshから出て来るストリームをunix側で受け取るって方法が有るな。 emacs+gosh あるいはreplの中だけで考えてると駄目って事だな。

debian:tmp$ gosh genstub |& grep -v 168
 :
#?-    "stdlib_current_vm"
#?-    "stdlib_vm_dump"
#?-    "stdlib_vm_trace_stack"
#?-    "stdlib_vm_instructionP"
#?-    "stdlib_closure_code"
#?-    "stdlib_procedure_info"

このパイプを書いてみて、debug-printがstderrに流れてる事に初めて気づいた。だめじゃん。