rapid-schemeをGambitのR4RSに移植する

rapid-scheme(R7RSで実装されたR7RSフロントエンド)がリリースされたので、Gambitに移植してみた。

rapid schemeはR7RSプログラムを入力し、マクロが展開された一本のR7RSプログラムを出力する。GambitはR5RSだけど、まぁその辺は気合で適当に。。最初はGambit付属のsyntax-rulesを使ったものの、ランタイムライブラリが正常にコンパイルされず単体のexecutbableにできなかったので、Gambit内蔵のdefine-macroに移植した。というわけでほぼR4RS部分しか使っていない。

rapid schemeをGambitに移植したのは、yuniの実装を進める上で実際に動作するフロントエンドで一度試しておきたかったため。自分でもyuni上のフロントエンドを実装しつつあるけど、(単体では)R7RS完全準拠にするつもりはあまり無いし、まだsyntax-rulesの実装が終ってないので。平たく言えば、GambitのFFIの評価を前倒しでやっておきたかったところにrapid-schemeが来た。なんというグッドタイミング。
現状でも、単体でR7RSスクリプトがそれなりに動くところまでは来ている。まだまだ未実装の手続きが多く残っているけど、rapid-schemeのテストはread系を除いて全て通っている。(readのテストはequal?がR6RS/R7RSのように循環構造で停止することを前提としているためまだ動かない) yuniのテストもevalとmatchが動かないくらいで他は大体動いている。
最大の問題は、マクロを完全に展開してしまうのでコードが膨張しコンパイルに死ぬほど時間が掛かる点。手元のPCでインタプリタをビルドするのに5分くらいかかる。普通にスクリプトを実行する分にはあまり困らないけど。。たぶん追加の最適化パスを書かないとコンパイラとして実用するのは厳しいと思う。
一応、現状のビルドでもlibgambitgscをリンクしていて、Gambitのコンパイラ機能(compile-file等の手続き)は一通り使えるようにしてある。真面目にコンパイラとして機能させるためには、ランタイムライブラリとかをどう配置するのかを考える必要がある。

LarcenyでwriteするとR6RS要素が出てくるのでGambitで正常に読めない問題

... セクション名でもう言い切ってしまっているが、rapid-scheme自体はexpandするためにR7RS処理系を使ってbootstrapする必要がある。rapid-scheme推奨の処理系であるLarcenyを使ったが、これはR6RSでもあるので、expand後のコードをwriteするとsyntaxのようなR6RS要素もabbreviateしてしまう。

(g6039_make-rtd '<expression> '##'aux #f)
;; (g6039_make-rtd '<expression> '#(syntax aux) #f) の意味。Gambitでもエラー無く読めるが違う内容になる。

というわけで、Larceny上では何故かwrite-sharedするとabbreviateされないという挙動を利用し、かつ、write時に一旦コードをconsしなおして全体をコピーすることにした。

vectorとbytevectorがself-evaluateでない問題

R5RS/R6RSではvectorやbytevectorはself-evaluateオブジェクトでないため、プログラム中に書くにはquoteする必要がある。rapid-schemeは普通にこれらをquoteしない形で出力するため、ちょっとしたマクロ展開器を用意してそこで一緒に処理することにした。
ちなみに、Gambitにはcase-lambdaやlet-valuesも無いのでこれらも展開している。

(define (%%expand-input frm)
  ;; Pre-process form with simply-minded macro expander
  ;; which only supports:
  ;; 
  ;;  (quote frm)
  ;;  #(v ...)    - Self evaluating vector
  ;;  #u8(b ...)  - Self evaluating bytevector(u8vector on Gambit)
  ;;  let-values
  ;;  set!-values
  ;;  case-lambda
  ;;
  ;; it assumes alpha transform which should be done on
  ;; rapid-scheme frontend
  (define (do-expand orig head body)
    (case head
      ((quote) orig) ;; short cut
      ((let-values)
       (%%expand-input (apply %%expand-let-values body)))
      ((set!-values)
       (%%expand-input (apply %%expand-set!-values body)))
      ((case-lambda)
       (%%expand-input (apply %%expand-case-lambda body)))
      (else (cons head (map expand-1 body)))))

  (define (expand-1 frm)
    ;(pp (list 'EXPAND: frm))
    (cond
      ((and (pair? frm) (list? frm))
       ;; We don't have any interest on dotted-lists
       ;; (they're only appeared as (lambda (a . b) ...) etc)
       (let ((a (car frm))
             (d (cdr frm)))
         (cond
           ((pair? a)
            (map expand-1 frm))
           (else (do-expand frm a d)))))
      ((vector? frm)
       `(quote ,frm)) ;; ベクタが出てきたらquoteする
      ((u8vector? frm)
       `(quote ,frm)) ;; bytevectorが出てきたらquoteする
      (else frm)))
(expand-1 frm))

Gambit上の細かいデバッグ手法

とりあえず環境変数GAMBOPTにdrRと入れる

  • export GAMBOPT=drR

これで、例外の発生時に止まるので,bでbacktrace、,iで展開後のprocedureを表示。
安定して動作するまでは、組込み手続きの break でブレークポイントを設定できるので、

(define member
  (case-lambda
    (() (break member) (member)) ;; HACK(breakpoint)
    ((x lis) (member x lis equal?))
    ((x lis p)
     (and (pair? lis)
          (or (and (p x (car lis))
                   lis)
(member x (cdr lis) p))))))

のように、適当な手続きを特殊な方法で呼ぶとbreakするようにしてブレークポイントを作っていた。↑の場合は、引数無しで (member) のように呼ぶと、その場でブレークする。
専用のbreak手続きを用意しないのは、rapid-scheme側のコードを極力変更したくなかったため。既存の手続きをのっとる形なら、プログラム側のimportを変更しないで済むし、(scheme base)にある手続きなら大抵のところで使用できる。