2012-12-31

2012年を振り返って

今年はなんだかいろいろ激変した感じがある年ではあるが、プライベートなことを振り返ると気分がへこむのでとりあえずはSagittarius周りのことだけで。

【達成したこと】
  • R6RSマクロ展開器が標準により準拠した
  • R7RS対応がほぼ終わった
  • 月1リリースの継続
あまり目標を立ててないので、自分の中で達成したというのはないなぁ。自分がほしい機能をいっぱい詰め込んだ「俺々処理系」度がえらく進んだ感があるが(リーダの置き換えとか)。


【来年やりたいこと】
  • ドキュメント
  • ライブラリの充実
  • 使い勝手が悪い(と個人的に思っている)部分の改善
ドキュメントは書いてないというのもあるのだが、1個の巨大なファイルになっているのが気になってきたのでその辺も直したい。
ライブラリは仕事で使う分でも足りないなぁと思う部分が出てきたので(主にバイナリいじるようなの)その辺を充実させていきたい。industriaのpackみたいなのがあると汎用的に使えるか?
使い勝手は、僕個人の感想なのだが、使い捨てのスクリプトを書く際にいちいちimportを書くのがさすがに面倒だなぁと思ってきたので、その辺を何とかしたい。
処理系本体に大きな変更を加える気は今のところないのだが、0.5.0か0.6.0あたりでビルド時のオプションで自前のGCを選択できるようにしたいと思っている。ただ、そこまで個人的にやれるかというのはかなり疑問なのであくまで「やりたい」程度ではあるが。

基本的に「必要ドリブン」(泥縄とも言う)開発スタイルなので、来年やりたいことというのは、現状で改善したいことでしかなく、下手をすれば1月中に終わるというものだったりもするのだが。

何はともあれ、来年もよろしくお願いいたします。

2012-12-18

TLVパーサがほしい

最近仕事でAPDUをいじる必要があるのだが(正確には眺めてるだけなんだけど・・・)、中に入ってるデータとかSIMが返すデータとかがTLV形式であることが多い。っで、TLV自体は別に何か特別な仕様というわけでもないのでこのパーサがあってもいいかなぁと思っていたりする。

問題は、現在既にあるASN.1パーサは基本的にはTLVパーサの上にASN.1のオブジェクトを被せるという形になっている点だろう。TLVだけに特化したのを別に作るのは馬鹿らしいのでこれを分離する方向にしたい。ということでちょっと実装方針を考えることにする。

現状のASN.1パーサはDERとBERに特化した形になっているので不定長のバイト列を処理することができるのだが、TLV形式だけならこれは不要になる。っが、これを外すことは当然だができない。また、DER、BERだとタグ部分が1バイトを超えることがあるが、他の形式だとたいてい1バイトである。(この辺ちょっと自信ない。現状の実装が一般的な気もする。)

となると、タグを読む、長さを読む、値を読む手続きとそれらから得られたデータから最終的なオブジェクトを構築する手続きを渡して、TLVパーサを作るような形にするといいだろうか? でも、そこまでやるなら分離する必要ないよな?オブジェクトの構築だけを外出しにすると、今度は不定長データの処理に困るし、どうしようかな。

2012-12-15

Enbug

かなり大きめなバグを0.4.0で埋め込んだみたいである。問題になるのは以下のコード。
(define (foo)
  (define (bar n) (print n))
  (bar 1)
  (let ()
    (define (bar n) (print 'hoge))
    (bar 1)))
何の変哲もないコードだが、0.4.0では「CレベルのASSERTで落ちる」という脅威の振る舞いをしてくれる。

マクロ直しの一環でのEnbugなのだが、正直全く気づかなかった。久しぶりにベンチマークでも取るかとGambitベンチマークを走らせてはじめて発覚。この問題の面白い(いやな)ところは、実行されたインストラクションから見ると問題の箇所はラムダリフティングに見えた点。実はそこではなく、barが2箇所で使われている点にある。

なぜこんなバグを混入することになったかといえば、R6RS及びR7RSではdefine-syntaxが内部defineと同様に書けることに起因する。たとえば以下のコード。
(let ()
  (define even?
    (lambda (x)
      (or (= x 0) (odd? (- x 1)))))
  (define-syntax odd?
    (syntax-rules ()
      ((odd?  x) (not (even? x)))))
  (even? 10))
こんなコードがあった際に、コンパイラはまずマクロのコンパイルをする。その後、内部defineのコンパイルをする。っが、実は一箇所大きめの落とし穴があって、どちらのコンパイルの際でも内部defineの名前を局所変数として登録する。つまり2重登録が発生する。っで、これの解決の方法がまずかった。

2重で登録されているのなら、2つ目に来るものは無視すればいいのだろうと安易に考えたのだが、そのをしたら最初のコードが動かなくなった。ある意味当たり前である。同名がくることは一切考えていなかったのだから。

リリース直後に気づいた不具合の致命的度としては最大級な気がする・・・orz

2012-12-14

Sagittarius 0.4.0リリース

Sagittarius Scheme 0.4.0がリリースされました。ダウンロード
今回のリリースはマイナーバージョンアップリリースです。大きな変更点は以下、
  • R6RS準拠度(マクロ周り)の改善
  • R7RS small (draft 8)の要求仕様への準拠
修正された不具合
  • datum->syntaxが与えられたテンプレート識別子を解決しない不具合が修正されました。
  • 深くネストしたリストを出力するとSEGVを起こしていた不具合が修正されました。
  • import句でのforキーワードが無視されていた不具合が修正されました。
    • ただし、run、expandなどのフェーズ指定句は従来どおり無視されます。
  • let-syntax及びletrec-syntaxがR6RSモードでスコープを作っていた不具合が修正されました。
  • カスタムコーデックがtranscoded-portで使用できない不具合が修正されました。
  • path-map及びpath-for-eachに:all #fを渡した際に処理が終わらない不具合が修正されました。
  • bytevector-u64-set!及びbytevector-s64-set!が値を正しくセットしない不具合が修正されました。
  • port-eof?がカスタムポートに対して使用できない不具合が修正されました。
  • random-integerが与えられたサイズと同じ値を返すことがある不具合が修正されました。
  • (eqv? 0.0+0.0i 0.0-0.0i)が#tを返す不具合が修正されました。
改善点
  • マクロのR6RS準拠度が大幅に改善されました。
  • リーダマクロがファイル単位からポート単位に変更になりました。
  • 4096文字を超える文字列の読み取りが可能になりました。
  • Debian Linuxがサポートされました。
新たに追加されたライブラリ
  • SRFI-25及びSRFI-78が追加されました。
    • ただしSRFI-78はcheck-ecがR6RSモードでは動きません。

2012-12-12

パターン変数

リリース直前にバグに気づいた。もちろんマクロ周り・・・
問題となるのは以下のようなコード。
(define-syntax loop
  (lambda (x)
    (syntax-case x ()
      [(k e ...)
       (with-syntax
           ([?break (datum->syntax #'k 'break)])
         #'(call-with-current-continuation
            (lambda (?break)
              (let f () e ... (f)))))])))
(let ((n 3) (ls '()))
  (loop
   (if (= n 0) (break ls))
   (set! ls (cons 'a ls))
   (set! n (- n 1))))
R6RSテストスイーツでは?breakの部分はbreakになっている。これがすごく長い間誤解を招いていた。何が問題かといえばコードではなく、(もちろん)実装の方で、現状の実装ではマクロ展開後の式をもう一回総舐めするのだが、その際に上記のbreakを正しいものに置き換える処理をしている。ただ、その際にパターン変数の中身ではなく、パターン変数そのものを見て置き換えるので上記のコードだと?breakが対象になる。

もちろん、よく考えればそれはおかしいと気づくのだが、なぜかずっと現状の実装が正しいと思い込んでいた。っで、R6RSへの準拠度を高めようとマクロの書き直しを終えた後に、いろいろなマクロを試していて気づいたと・・・orz

気づいたからには直さないと気が済まないので現在修正中なのだが、ほぼ全てのテストは通るのに、SRFI-86のテスト1個だけが通らない。他のテストは勘所などが分かったのだが、こいつは長巨大マクロライブラリなので全く分からん。まず間違いなく誤参照をしているのだけど、どこがそれを引き起こしているのかが分からない。

リリースを引き伸ばすか、こいつは次のリリースで直すか悩むところである。R7RSのドラフト8対応と称して出してしまおうか・・・(悪魔の囁き

2012-12-10

R7RS 8thドラフト斜め読み

7thドラフトから1ヶ月しか経っていないのにもう8thが出るとは。ちょっと油断していた。

今回のドラフトは7thのtypoと非正確数のeqv?あたりぐらいだろうと思っているので軽く斜め読み。Chibi-Schemeもこれにあわせてか、0.6.1とバグフィックス版を出してきている。

前回からの変更点
  • (eqv? 0.0 -0.0)が処理系が負の0.0を識別するなら#fと明示された
  • read-bytevector!の引数順が変更になり、それに伴ってオプショナル引数が4つに増えた
これくらいしか見つけられなかった。さすがに斜め読みすぎたか。eqv?の変更はメーリングリストでかなり険悪な状態までいっていたりしてどう落ち着くのか不安ではあったが、R6RSとの整合性が保たれていたのでちょっと安心。でも、未だに(eqv? 0.0 -0.0)が#fでなければならないプログラムを見たことないが、atanとかバリバリ使う人だと問題になるんだろうか?

やべ、こんなに変更点がないとは思っていなかった。書くことがないw

とりあえず、宣伝。
0.3.8でドラフト7に対応したんだけど、Chibiのテストが結構こけてたりしてたんだけど、次のリリースでは全て問題なく通る。それにプラスして、Chibiでは(たぶん、まだ)サポートされていないinclude-library-declarationsもサポートされているので、現状ではR7RS Smallをフルサポートしている処理系ということになる。今週末リリースする予定(あくまで予定)。

2012-12-09

Rewrote case-lambda

Sometimes it's good to review what I've done so far. Yes, I've found something not efficient and it can be more efficient. This time it was case-lambda.

The original was SRFI-19 and since R6RS it's a standard macro (or syntax) for Scheme. However neither of implementations is efficient. At least it can be written better in R6RS. The problem of the reference implementation is it creates closures per lambda formals and apply it with given arguments. If the implementation is smart enough, the apply could be inlined however Sagittarius is not so smart (unfortunately).

If the implementation is not smart enough, then programmers need to be smart to let compiler emit efficient codes. So I rewrote it. This is what I think at least better than the references.
;; this is almost only R6RS except reverse!
(define-syntax case-lambda-aux
  (lambda (x)
    (define (construct args formals clause*)
      (define _car #'car)
      (define _cdr #'cdr)
      (define (parse-formals formal args inits)
        (syntax-case formal ()
          (() (reverse! inits))
          ((a . d)
           (with-syntax ((arg `(,_car ,args))
                         (args `(,_cdr ,args)))
             (parse-formals #'d #'args
                            (cons (list #'a #'arg) inits))))
          (v
           (reverse! (cons (list #'v args) inits)))))
      (with-syntax ((((var init) ...) (parse-formals formals args'()))
                    ((clause ...) clause*))
        #'(let ((var init) ...) clause ...)))
    (syntax-case x ()
      ((_ args n)
       #'(assertion-violation #f "unexpected number of arguments" args))
      ((_ args n ((x ...) b ...) more ...)
       (with-syntax ((let-clause (construct #'args #'(x ...) #'(b ...)))
                     (expect-length (length #'(x ...))))
         #'(if (= n expect-length)
               let-clause
               (case-lambda-aux args n more ...))))
      ((_ args n ((x1 x2 ... . r) b ...) more ...)
       (with-syntax ((let-clause (construct #'args #'(x1 x2 ... . r) 
                                            #'(b ...)))
                     (expect-length (length #'(x1 x2 ...))))
         #'(if (>= n expect-length)
               let-clause
               (case-lambda-aux args n more ...))))
      ((_ args n (r b ...) more ...)
       #'(let ((r args)) b ...)))))

(define-syntax case-lambda
  (syntax-rules ()
    ((_ (fmls b1 b2 ...))
     (lambda fmls b1 b2 ...))
    ((_ (fmls b1 b2 ...) ...)
     (lambda args
       (let ((n (length args)))
         (case-lambda-aux args n (fmls b1 b2 ...) ...))))))
The trick is really simple. Just let macro compute the given arguments. Even length is called only once.

With this code, at least on Sagittarius, the compiler emits better code than references (there is no extra closure created).

2012-12-08

最後の砦

マクロの書き換えを始めて1週間(以上か?)、大詰めまで来ている気がする。美しくなかった機能をばっさりと切り捨て、マクロ展開器は(個人的に)かなりシンプルになったと思う。既存のテストが全て通るようになり、いよいよ本丸のindustriaを走らせて見たら、以前と同じエラーで落ちる。これは何かを見落としているなと思い、落ちている原因のdo/unrollマクロを眺めることにした。

正直、中で何をしているのか理解するのに時間がかかるくらいでかく複雑(に僕には見える)マクロだが、本質的な原因は以下のマクロが動かないことにあることが分かった。
(import (rnrs))
(define-syntax expand-it
  (lambda (x)
    (define (gen-return var) (with-syntax ((v var)) #'v))
    (syntax-case x ()
      ((_ (v init) expr ...)
       (with-syntax ((r (gen-return #'v)))
         #'(let ((v init))
             (when (= v r)
               expr ...)))))))

(expand-it (v 1) (display 'ok) (newline)) 
Chezで試したが当然動く。正直、何がいけないのかすでに分かっていて、以前あったパターン変数の問題の展開された識別子版である。
入力として与えられてvが二箇所のsyntaxで展開されているためにそれぞれ別の識別子として出力されるのが問題なのである。

それこそ、マクロ展開器との格闘を始めて結構長いと思うが、このパターンは完全に見落としていた。こんなマクロ書いたことないし、見たこともないからである。パターン変数の問題はいくらか書いたし、見たことがあったので何が原因かも分かっていたのだが、これは思いもよらなかった。なんというか、目の前に大穴が空いているのに全く気づかなかった気分である。

パターン変数の際と同様の方法で解決するとするか。

しかし、この1週間でマクロ展開の実装は所詮環境の参照と識別子のリネームだけなんだと痛感させられた。ただ、それが異様に面倒なだけで・・・

2012-12-05

expanded internal define

It's aboud the macro expander of Sagittarius Scheme. I'm re-writing it to get better conformity with R6RS. Now I've got a (well not only one) problem and found a bug of current version.

Short description of the bug;
;; Doesn't matter what equal? return.

;; Bug 1
(let ()
  (define-record-type (pare kons pare?)
    (fields (mutable x kar set-kar!)
     (immutable y kdr)))
  (define a (kons (vector 1 2 3) '(a b c)))
  (define b (kons (vector 1 2 3) '(a b c)))
  (equal? a b))
kons ;; -> #<closure kons>
;; this should be &assertion of unbound variable

;; Bug 2
(let ((c #t))
  (define-record-type (pare kons pare?)
    (fields (mutable x kar set-kar!)
     (immutable y kdr)))
  (define a (kons (vector 1 2 3) '(a b c)))
  (define b (kons (vector 1 2 3) '(a b c)))
  (equal? a b))
;; -> &assertion
The first one is obviously wrong, it broke the definition of internal define. The second one is releated to comple time environment lookup. The kons is defined by define-record-type and it must be the same identifier as the one in internal define. But it's not. That's because macro expander re-name it to different identifer.

The simplest solution I have tried was to wrap all given expression first then expand macros. It's simple it worked (mostly) but broke constant literals such as quoted lists or vectors. The problem was not only it but also broke some other codes (mostly related with er-macro-transformer used for generating bootstap code and R7RS). So I've decided not to do this (even though I feel like it's the easier way to do it).

The second solution is modifying the environment lookup process. An identifier can hold some environment without any restriction on current branch and if I can find a better way to look up proper variable from compile time environment, it can be resolved. The problem is I don't know how...

Consider the case of Bug 1. The kons and environment have following frames;
;; kons identifier's frame
((0))
;; when 'a' is being compiled
((0 (kons#user . #(lvar ...)) ...) ;; kons#user is an identifier
 (0)) ;; &lt-- the same environment as the kons identifier
So when a is being compiled, compiler can see the same environment at the very top of the frame chain. But how can we assume kons#user and kons in internal define are the same variables? If the root of the frame are the same, would we be able to assume? Or when it's expanded to internal define, should it be stripped to mere symbol?

Hmm..., I need to think ...

2012-12-02

SchemeでReader Macro

この記事はLisp Reader Macro Advent Calendar 2012の記事として書かれました。

3日目はSchemeでリーダマクロを使ってみます。

僕が知る限り、Schemeの仕様にはユーザ定義のリーダマクロはありません。しかし、いくつかの処理系は独自にそれらを定義する方法をもっています。僕が知る限りではRacket、Gambit、Chicken、Sagittarius(拙作)は独自のリードテーブル拡張手続きを持っています。前2つはあるということくらいしか知らないので、ここでは拙作Sagittariusのリーダマクロの簡単な使い方、及びこれが使えると何がうれしいかということを宣伝を兼ねて紹介します。

世界的にはRacketがよく使われている(らしい)のですが、日本でSchemeと言えばGaucheがまず挙げられるのではないでしょうか。しかし、GaucheはR5RS準拠の処理系ということで、R6RSで書かれたライブラリを実行することが不可能です(2012年12月3日現在)。でも、Gaucheの独自拡張されたリーダマクロを使いつつ、R6RSのライブラリも使いたい!そんなこと考えたことありませんか?そこでSagittariusの出番です(宣伝ここまで)。

SRFI-14な文字セットをリード時に読み込むことを考えます(*1)。Gaucheでは以下のように書けます。
#[a-zA-Z] ;; -> 文字セット(#[a-zA-Z]と表示されます)
これはGaucheの独自拡張なので、他のScheme処理系とは互換性がありません。でも便利そうですよね?じゃあ、こう書けるようにしてみましょう!
(library (char-set reader)
    (export :export-reader-macro)
    (import (rnrs) (sagittarius reader)
            (srfi :14))

  (define-dispatch-macro charset-reader #\# #\[
    (lambda (port subc param)
      (let loop ((cs (char-set-copy char-set:empty)))
        (let ((c (get-char port)))
          (if (char=? c #\])
              cs
              (let ((nc (lookahead-char port)))
                (cond ((char=? nc #\-)
                       (get-char port)
                       (let ((c2 (get-char port)))
                         (if (char=? c2 #\])
                             (char-set-adjoin! cs c nc)
                             (loop (ucs-range->char-set!
                                    (char->integer c)
                                    (+ (char->integer c2) 1)
                                    #f
                                    cs)))))
                      (else
                       (loop (char-set-adjoin! cs c))))))))))
)
#!read-macro=char-set/reader
'#[a-zABC] ;; -> #<char-set #\A-#\C #\a-#\z>
#[a-]      ;; -> #<char-set #\--#\- #\a-#\a>
たったこれだけでGauche互換な文字セットリーダが書けます(*2)。
2日目の記事を読まれた方には簡単すぎるかもしれませんが、簡単な解説です。上記のコードは#\##\[の組み合わせをリーダマクロとして使用することを宣言しています。(CLでいうset-dispatch-macroと同義。)定義はリファレンスマニュアルを参照していただくとして、問題の中身です。コードを見ればすぐに分かるレベルの単純なものですが、最初に空の文字セットをコピーして読み取った文字、もしくは文字範囲(#\-でつながっている文字)を文字セットに破壊的に追加していき、#\]を読むまで続けます。(ちなみに、EOFまで読んでしまうとchar=?&assertionを投げます。あまりよくない振る舞いですので、実用する際はeof-object?で検出して適切な例外を投げた方がいいでしょう。)

しかし、最新のリリース(0.3.8)で上記のコードを使うとキャッシュを壊すので以下のように書く必要があります。
(library (char-set-good)
    (export :export-reader-macro)
    (import (rnrs) 
            (sagittarius reader)
            (rename (only (srfi :1) alist-cons) (alist-cons acons))
            (srfi :14))

  (define-dispatch-macro char-set-reader #\# #\[
    (lambda (port subc param)
      (let loop ((ranges '()) (chars '()))
        (let ((c (get-char port)))
          (if (char=? c #\])
              `(char-set-union (string->char-set ,(list->string chars))
                               ,@(map (lambda (range)
                                        `(ucs-range->char-set 
                                          ,(car range) ,(cdr range))) ranges))
              (let ((codepoint (char->integer c))
                    (nc (lookahead-char port)))
                (cond ((char=? nc #\-)
                       (get-char port)
                       (let ((c2 (lookahead-char port)))
                         (cond ((char=? c2 #\])
                                (loop ranges (cons nc chars)))
                               (else
                                (get-char port)
                                (loop (acons codepoint (+ (char->integer c2) 1)
                                             ranges)
                                      chars)))))
                      (else
                       (loop ranges (cons c chars))))))))))
)
#!read-macro=char-set-good
(import (srfi :14))
#[a-zABC]  ;; -> (char-set-union (string->char-set "CBA") (ucs-range->char-set 97 123))
#[a-]      ;; -> #<char-set #\--#\- #\a-#\a>
これは、一部の組込みオブジェクトをキャッシュ機構がうまくキャッシュできないことに起因しています。次期リリースではある程度この問題が解決される予定です(少なくとも文字セットとハッシュテーブルは最新のHEADでは解決されています)。ユーザ定義のオブジェクトに対しては明示的にキャッシュとして書き出し、及び読み込みをするための手続きを定義することが可能です。

リーダマクロは処理系依存な機能な上に、処理系によっては(主にSagittariusですが)制限もあったりと使いどころが難しいものかもしれません。しかし、SRFI-105等のリーダ拡張を必要とするライブラリにScheme側のみで対応できたり、処理系の独自拡張の差異を吸収できたりと便利かつ強力な機能でもあります。

3日目はSchemeでもリーダマクロを使う方法を紹介しました。

*1: Sagittariusでも正規表現の読み込みははC側で実装されているので例として使いにくかったのです。
*2: 実際にはGaucheの文字セット読み取り機能はもっと多機能なのでこれだけだと完全に互換とはいえないですが・・・

R6RSとR7RSのライブラリ比較

この記事はLisp Advent Calendar 2012の3日目の記事として書かれました。

3日目は最近出たR7RSドラフトセミファイナル(とWG1が読んでいる)と、R6RSのライブラリシステムの文法的な違い(意味的なものも多少)を見ていこうと思います。

まずは、R6RSのライブラリのおさらいです。R6RSは制定されてすでに数年(2007年だったっけ?)経っているので、皆さんもうご存知ですよね。なので以下はごく簡単な例;
(library (foo)
   (export bar)
   (import (only (rnrs) define quote))
  (define bar 'bar))
#|
ライブラリ構文の定義
(library <library-name>
   (export <export-spec> ...)
   (import <import-spec> ...)
  <body> ...)
|#
R6RSのライブラリは暗黙もしくは明示的なマクロ展開フェーズを持ちます。これはマクロ内で使われる手続きと、ランタイムで使われる手続きを明示的に分ける必要があったから(らしい)です。(個人的にこのフェーズ分けは考えるのが面倒なので、暗黙的に解決してくれる処理系の方が好きです。好みの問題ですけど。)

次はR7RSのライブラリの例;
(define-library (foo)
  (import (scheme base) (scheme write))
  (begin (define foo 'foo)
         (define (print . args) (for-each display args)(newline)))
  (define (printw . args) (for-each write args)(newline))
  (export foo print)
  (export bar)
  (begin (define bar 'bar))
  (cond-expand
   ((library (srfi 1))
    (import (rename (only (srfi 1) alist-cons) (alist-cons acons))))
   ((library (srfi :1))
    (import (rename (only (srfi :1) alist-cons) (alist-cons acons))))
   (else
    (define (acons a b c) (cons (cons a b) c))))
  (export acons))
#|
ライブラリ構文の定義
(define-library <library-name>
   <library-declaration> ...)
|#
あえて違いを出すように書いてありますが、普通はexportとimport句はまとめると思います。
R7RSではR6RSと違い順番がライブラリ定義の順番及びその個数が規定されていません。なので、上記のコードは既存のR7RS処理系(ChibiとSagittariusしか知らない)で正しく動きます。(規定されていないので処理系依存になるかもしれません。)
R7RSのimport句はR6RSとは異なりforがありません。これはR7RSではマクロの展開フェーズが規定されていないからです。処理系は暗黙的にしかマクロ展開フェーズを持つことができません。(問題になる処理系の方が少ない気はしますが)
一方export句でもrenameのあり方がR6RSとは異なります。R6RSではrename句の中に複数の識別子を定義することができましたが、R7RSでは一つのrenameに対して一つの識別子しか定義することができません。
また、cond-expandはSRFI-0のものとは多少違い、featureとしてlibraryが追加されています。これによってライブラリが存在するかのチェックが可能になり、上記のように無ければ定義、みたいなことが可能(なはず)です。
また、以下の例はR7RSが既存のR5RSで書かれたプログラムをそのまま使用することが可能であるということを強く意識して作られたことを示す例です。
;; bazz.sls (for Sagittarius) or bazz.sld (for Chibi)
(define-library (bazz)
  (export bla)
  (import (scheme base))
  (include "bazz.scm"))

;;--
;; bazz.scm
(define bla 'bla)
ライブラリ内で使用されるinclude、include-ci(case insensitive)はライブラリ上にそのままファイルの内容が展開されるイメージです。処理系によっては以下のように解釈するかもしれませんが、まぁ無視できる程度の差異でしょう(Sagittariusはこのように解釈する)。
(define-library (bazz)
  (export bla)
  (import (scheme base))
  (begin (define bla 'bla)))
事実、Chibi-Schemeのほぼ全てのライブラリはincludeを用いて記述されています。また、今回のドラフトから追加されたinclude-library-delarationsは以下のように使えば処理系ごとの拡張子の違いを簡単に吸収することが可能です。(ただし、現状ではSagittariusのみがこの構文をサポートしている状態です)。
;; boo.sls or boo.sld
(define-library (boo)
   ;; もちろん、この上下にいろいろ書いてもOK
   (include-library-declarations "boo.decl"))

;;--
;; boo.decl
(export bah)
(import (scheme base))
(include "boo.scm")

;;--
;; boo.scm
(define bah 'bah)
include-library-declarationsはinclude、include-ciとほぼ同様の動作をしますが、include先のファイル(この場合はboo.decl)は<library-declarations>で構成されている必要があります。R7RSの議論の中ではこの構文は巨大なexport句を外出しにし共有化するために作られてのですが(*1)、このような使い方もできるのではと思っています。

R7RSはまだドラフトで上記の構文を試せる処理系はまだ少ないですが(2012年12月現在)、多くの処理系製作者がすでに追従するということを表明しています(*2)。個人的にはR7RSの構文の方がR6RSに比べて柔軟かつ処理系の差異も吸収しやすいのではと考えています。

3日目はR6RSとR7RSのライブラリ構文の比較を行いました。

*1: たとえば(srfi :1)と(srfi :1 lists)は同一のexport句を持ちますが、R6RSのライブラリではどちらのライブラリも同一のexport句を記述する必要があります。Sagittariusは:allキーワードがあるので、多少手が抜けたりしますが・・・
*2: Kawaはすでに可能な限り追従するとR7RSのメーリングリスト上で製作者が言っています。Larceny、Gauche、及びChickenも最初のドラフトの段階では追従するということを回答していました。参照

2012-12-01

Andre van Tonderの展開器を読む

やっぱりマクロ周りの問題は割りと大きくのしかかってくる感じなので、まじめに戦略を練るために既存の展開器を理解しようかなぁと思い読み始める。

とりあえず以下が理解したこと;
  • マクロ展開時にユニークなマークを作成
    • colorとなっているもの
    • これによってbound-identifier=?を実装している
  • 展開フェーズはdefine-syntaxが現れると+1される
    • Sagittariusは暗黙に解決する方向をとるので、これは要らない
  • syntax-caseのマッチはランタイムに生成される
    • パフォーマンス的にはうれしくないよなぁ
    • ただ、その分いろいろな情報解析で手を抜けそう
  • 式が渡された段階で全てのシンボルが識別子に変換されている
    • 重要だけど、リテラルなリストとかどうしようかな・・・
  • 環境は全ての束縛を保持する
    • 局所変数から大域変数、はたまた構文まで全部
    • free-identifier=?の実装には必要
      • 僕の考え方は間違ってなかったのかと納得
  • パターン変数を識別するものがある
っで、現状のSagittariusの実装とは大きく異なる部分;
  • パターンマッチは組み込みである
    • コンパイルが2度走る必要がないの多少高速なはず
  • テンプレート展開部分も組み込みである
    •  Andre van Tonderの方はこいつもマクロごとに生成される
  • ラップする式は束縛されている変数のみかつ、コンパイラが行う
    • しかも、複数回走る
    • 全体を1回だけにした方がいいが、リテラルリストをどうするか
ある意味当たり前なんだけど、マクロ展開の部分だけに目を向けるなら、syntax-caseとsyntaxだけが解決されている。つまり、それ以外のR6RSが提供する低レベルマクロ用何かしらは、すべてこの2つの組み合わせのみで解決できる。
ということは、この2つが完璧に動作さえすれば、他の動作は保障されているわけで、現状いろいろごにょごにょやっている部分がすっきりする可能性が高い。

Andre van Tonderの展開器ではポータビリティのために生成されるGUIDはシンボルなのだが、gensymがある処理系ならばこれはgensymに単純に置き換えることができる(検証もした)。ついでに、GUIDは一時変数とか、ユニークなシンボルの生成以外には使われていない。
ライブラリの関係(だけじゃないかも)で、グローバルもしくはローカルに束縛されたシンボルの実態はGUIDに変換されている。ただ、ルックアップ自体は識別子で行うので、Sagittariusではやる必要はないと思う。

全体を1回だけラップすることで起きえる問題を考えてみる;
  • リテラルリストが壊れる
    • 完全にリテラルだと分かっているならラップしないということも可能
    • quasiquoteが困りそうだがどうする?
      • ラップ時にquasiquoteだけ特別視?
    • ベクタもリテラルであるよな?
  • libraryやdefine-library構文に手を入れる必要がある。
    • exportやimportがシンボルを期待しているが、識別子になってしまう
      • まぁ、大きな問題ではない
リテラルリストだけなんとかすれば大きく問題はないと思う。ずっと使わずあるだけだったpass0手続きがついに使われるときが来たか。
気になるのは、ただでさえメモリ喰いなのにさらにメモリを喰うようになることか。読んだS式全部トラバースして、丸コピーしないといけなくなるから。まぁ、現状も似たようなことやってるし、考えるだけ無駄か。