M.Hiroi's Home Page

Functional Programming

お気楽 Scheme プログラミング入門

[ PrevPage | Scheme | NextPage ]

micro Scheme コンパイラの作成 (2)

今回は micro Scheme に「伝統的なマクロ」を追加します。インタプリタの場合、効率を考慮しないでよければ、伝統的なマクロを実装するのはとても簡単です。S 式を評価してマクロ展開したあと、その結果を再度評価すればいいのです。ただし、この方法は実行速度が遅くなるので実用的ではありません。

コンパイラの場合、プログラムをコンパイルする時にマクロ展開が行われるため、翻訳されたコードにはマクロが存在しません。その分だけプログラムを高速に実行することが可能になります。つまり、マクロを使うのであれば、インタプリタよりもコンパイラの方が有利なのです。

●マクロの定義

マクロはシンタックス形式 define-macro で定義します。マクロの本体はラムダ式で表します。そして、リストに次の形式で格納します。

(macro closure code env)

ラムダ式を評価してクロージャを作り、それがマクロであることを表すため、その先頭にシンボル macro を付加します。この処理を仮想マシンの命令 defm で行います。

define-macro のコンパイルは関数 comp に次のプログラムを追加するだけです。

リスト : define-macro のコンパイル

(define (comp expr env code)
  ...
        ((eq? (car expr) 'define-macro)
         (comp (caddr expr) env (list* 'defm (cadr expr) code)))
  ...
)

処理内容は define のコンパイルと同じです。仮想マシンの命令が defm になるだけです。

●マクロのコンパイル

次は関数 comp にマクロをコンパイルする処理を追加します。プログラムは次のようになります。

リスト : マクロのコンパイル

;;; マクロか
(define (macro? expr)
  (let ((val (assoc expr *global-environment*)))
    (and val (pair? (cdr val)) (eq? 'macro (cadr val)))))

;;; マクロのコードを取り出す
(define (get-macro-code expr)
  (caddr (get-gvar expr)))

;;; コンパイラ本体
(define (comp expr env code)
  ...
        ((macro? (car expr))
         ;; マクロ展開してからコンパイルする
         (let ((new-expr (vm '()
                             (list (cdr expr))
                             (get-macro-code (car expr))
                             (list (list '() '() '(stop))))))
           (comp new-expr env code)))
  ...
)

expr の先頭要素がマクロであるか関数 macro? でチェックします。micro Scheme の場合、マクロ定義は大域変数に格納されています。*global-environment* に expr があり、その値がリストでかつ先頭要素が macro であればマクロです。マクロ本体のコードを仮想マシン vm で実行して、新しい S 式 new-expr を求めます。このとき、マクロの引数 (cdr expr) は評価しないで、そのまま仮想マシンの環境レジスタ E にセットします。

マクロのコードは関数 get-macro-code で求めます。マクロのコードは命令 rtn で終了しているので、ダンプレジスタ D に仮想マシンを停止する命令 stop を入れておきます。あとはマクロ展開された S 式 new-expr を comp でコンパイルすればいいわけです。これでコンパイルするときに全てのマクロが展開されます。

●バッククオートの処理

次はバッククオートを処理する quasiquote をマクロで定義します。拙作のページ Scheme で作る micro Scheme (2) で作成したインタプリタでは、quasiquote をシンタックス形式として定義しましたが、コンパイラの場合はマクロを使って定義したほうが簡単です。

バッククオートで使う記号 (` , ,@) は省略形で、次に示す S 式に変換されます。

`(...) : (quasiquote (...))
,expr  : (unquote expr)
,@expr : (unquote-splicing expr)

バッククオートの中でシンボル unquote と unquote-splicing があれば、引数 expr を評価するように S 式を生成します。それ以外の場合は quote を付けて、引数を評価しない S 式を生成します。quasiquote はマクロなので、生成した S 式が再度評価されます。このとき、バッククオートの処理を行うことができます。プログラムは次のようになります。

リスト : バッククオートの処理

(define transfer
  (lambda (ls)
    (if (pair? ls)
        (if (pair? (car ls))
            (if (eq? (caar ls) 'unquote)
                (list 'cons (cadar ls) (transfer (cdr ls)))
              (if (eq? (caar ls) 'unquote-splicing)
                  (list 'append (cadar ls) (transfer (cdr ls)))
                (list 'cons (transfer (car ls)) (transfer (cdr ls)))))
          (list 'cons (list 'quote (car ls)) (transfer (cdr ls))))
      (list 'quote ls))))

(define-macro quasiquote (lambda (x) (transfer x)))

実際の処理は関数 transfer で行います。ls がリストでその先頭要素がリストの場合、先頭の要素が unquote もしくは unquote-splicing であれば、その引数にクオートをつけません。そして、残りのリスト (cdr ls) を transfer で変換し、unquote であれば cons で結合する S 式を、unquote-splicing であれば append で結合する S 式を生成します。これで、unquote と unquote-splicing の引数を評価することができます。それ以外の場合は quote を付けて引数を評価しない S 式を生成します。

ここで、関数 transfer の簡単なテストを行ってみましょう。次の実行例を見てください。

gosh[r7rs.user]> (transfer '(a b c))
(cons 'a (cons 'b (cons 'c '())))
gosh[r7rs.user]> (transfer '(,a b c))
(cons a (cons 'b (cons 'c '())))
gosh[r7rs.user]> (transfer '(,a ,@b c))
(cons a (append b (cons 'c '())))
gosh[r7rs.user]> (transfer '(,(car a) ,@(cdr b) c))
(cons (car a) (append (cdr b) (cons 'c '())))

このように、transfer を評価するとリストを生成する S 式が生成されます。unquote や unquote-splicing の引数はクオートが付いていないので、S 式を評価するときにその引数が評価されます。コンパイラの場合、この生成された S 式を再度コンパイルすることで、バッククオートの処理を行うことができます。

なお、今回のバッククォートの動作は Scheme の仕様書 (R5RS など) とは異なります。このあと定義するマクロが動作するだけの簡略版にすぎません。詳しい説明は micro Scheme コンパイラの作成 (4) : バッククォートの修正 をお読みください。

●簡単な実行例

それでは、簡単な実行例を示します。プログラム (secd.scm) だけではなく、micro Scheme 用のライブラリ (mlib.scm) もロードしてください。

>>> (define a '(1 2 3))
Compile => (ldc (1 2 3) def a stop)
Value => a

>>> `(a b c)
Compile => (ldc a ldc b ldc c ldc () args 2 ldg cons app args 2 ldg cons app args
 2 ldg cons app stop)
Value => (a b c)

>>> `(,a b c)
Compile => (ldg a ldc b ldc c ldc () args 2 ldg cons app args 2 ldg cons app args
 2 ldg cons app stop)
Value => ((1 2 3) b c)

>>> `(,@a b c)
Compile => (ldg a ldc b ldc c ldc () args 2 ldg cons app args 2 ldg cons app args
 2 ldg append app stop)
Value => (1 2 3 b c)

`(a b c) は a, b, c, をリストに格納するコードにコンパイルされて、値は (a b c) になります。`(,a b c) は a の値と b, c をリストに格納するコードにコンパイルされるので、値は ((1 2 3) b c) になります。`(,@a b c) は a の値を append で連結するコードにコンパイルされるので、値は (1 2 3 b c) になります。

もちろん、unquote と unquote-splicing の引数には関数を与えることもできます。次の例を見てください。

>>> `(,(car a) b c)
Compile => (ldg a args 1 ldg car app ldc b ldc c ldc () args 2 ldg cons app args
 2 ldg cons app args 2 ldg cons app stop)
Value => (1 b c)

>>> `(,(cdr a) b c)
Compile => (ldg a args 1 ldg cdr app ldc b ldc c ldc () args 2 ldg cons app args
 2 ldg cons app args 2 ldg cons app stop)
Value => ((2 3) b c)

>>> `(,@(cdr a) b c)
Compile => (ldg a args 1 ldg cdr app ldc b ldc c ldc () args 2 ldg cons app args
 2 ldg cons app args 2 ldg append app stop)
Value => (2 3 b c)

`(,(car a) b c) は (car a) が評価されるコードが生成されるので、値は (1 b c) になります。`(,(cdr a) b c) は (cdr a) が評価されるので、値は ((2 3) b c) になります。`(,@(cdr a) b c) は (cdr a) の評価結果を append で連結するので、値は (2 3 b c) になります。

●set! と eqv? の追加

次はマクロを作るときに使用するため、関数 eqv? と set! を作ります。set! は変数に値を代入します。この処理を実現するため、仮想マシンの命令に lset と gset を追加します。lset と gset の状態遷移を示します。

(v . s) e (lset (i . j) . c) d => (v . s) e c d
更新処理 : (set-lvar! e i j v)
(v . s) e (gset sym . c) d => (v . s) e c d
更新処理 : (set-gvar! sym v)

set! は変数に代入した値をそのまま返すことにします。

set! のコンパイルは次のようになります。

リスト : set! のコンパイル

(define (comp expr env code)
  ...
        ((eq? (car expr) 'set!)
         (let ((pos (location (cadr expr) env)))
           (if pos
               ;; 局所変数
               (comp (caddr expr) env (list* 'lset pos code))
             ;; 大域変数
             (comp (caddr expr) env (list* 'gset (cadr expr) code)))))
  ...
)

expr の先頭要素が set! の場合、関数 location で第 2 要素 (cadr expr) が局所変数にあるか探します。見つかった場合は、コード (list* 'lset pos code) を生成し、そこに第 3 要素 (caddr expr) を comp でコンパイルしたコードを追加します。大域変数の場合、命令は lset ではなく gset になります。

次は仮想マシン vm に lset と gset の処理を追加します。

リスト : 変数の更新処理

;;; 局所変数の値を更新する
(define (set-lvar! e i j val)
  (if (<= 0 j)
      (set-car! (drop (list-ref e i) j) val)
    (if (= j -1)
        (set-car! (drop e i) val)
      (set-cdr! (drop (list-ref e i) (- (+ j 2))) val))))

;;; 大域変数の値を更新する
(define (set-gvar! sym val)
  (let ((cell (assoc sym *global-environment*)))
    (if cell
        (set-cdr! cell val)
      (error "unbound variable " sym))))

;;; 仮想マシン
(define (vm s e c d)
  (case (pop! c)
    ...
    ((lset)
     (let ((pos (car c)))
       (set-lvar! e (car pos) (cdr pos) (car s))
       (vm s e (cdr c) d)))
    ((gset)
     (set-gvar! (car c) (car s))
     (vm s e (cdr c) d))
    ...
  ))

局所変数の更新は関数 set-lvar! で行います。通常の引数は set-car! でフレームの値を破壊的に修正します。可変個引数の場合はちょっと複雑です。j の値が -1 の場合、フレーム自身が変数を表すので、フレームを格納しているリストの値を set-car! で破壊的に修正します。簡単な例を示しましょう。

((lambda x (set! x 10)) 1 2 3)
E: ((1 2 3))
(set! x 10)
E: (10)

((lambda x (set! x 10)) 1 2 3) を呼び出すと環境 E は ((1 2 3)) になります。この場合、x の位置は (0 . -1) で値は (1 2 3) になります。set! で x の値を 10 に書き換えると、x の値はフレームそのものなので、フレームを格納しているリストの 0 番目の要素を破壊的に修正し、環境 E は (10) になります。

それ以外の場合、通常の引数以降のフレームが変数の値になるので、set-cdr! でフレームを破壊的に修正します。次の図を見てください。

((lambda (a b . c) (set! a 10) (set! b 20) (set! c 30)) 1 2 3 4 5)
E: ((1 2 3 4 5))
((set! a 10) (set! b 20) (set! c 30))
E: ((10 20 . 30))

((lambda (a b . c) ...) 1 2 3 4 5) を呼び出すと環境 E は ((1 2 3 4 5)) になります。この場合、a の値は 1 で b の値は 2 になり、c の位置は (0 . -3) で値が (3 4 5) になります。ドットリストで表すと、(1 2 . (3 4 5)) になるわけです。したがって、変数 a, b, c の値を書き換えると環境 E は ((10 20 . 30)) になります。

大域変数の更新は関数 set-gvar! で行います。assoc で変数と値を格納しているコンスセル cell を求めます。そして、set-cdr! で値を val に書き換えます。見つからない場合は error でエラーを送出します。

あとは *global-environment* に eqv? を追加します。

リスト : 初期化処理

;;; 大域変数
(define *global-environment*
        (list
          (list 'car   'primitive car)
          (list 'cdr   'primitive cdr)
          (list 'cons  'primitive cons)
          (list 'eq?   'primitive eq?)
          (list 'eqv?  'primitive eqv?)
          (list 'pair? 'primitive pair?)
        ))

●let

それでは実際にマクロを使ってみましょう。なお、これから示すプログラムは拙作のページ Scheme で作る micro Scheme (2) で作成したマクロとまったく同じです。インタプリタでもコンパイルでもマクロは正常に動作します。

まずは let を作ります。let は次に示すようにラムダ式を使って実現することができます。

(let ((a 0) (b 1) (c 2) ...) body  ...)
                   │
                   ↓ 
((lambda (a b c ...) body ...) 0 1 2 ...)

これをマクロでプログラムすると次のようになります。

リスト : let

;;; 関数 cxxr
(define cadr (lambda (x) (car (cdr x))))
(define cdar (lambda (x) (cdr (car x))))
(define caar (lambda (x) (car (car x))))
(define cddr (lambda (x) (cdr (cdr x))))

;;; let
(define-macro let
  (lambda (args . body)
    `((lambda ,(map car args) ,@body) ,@(map cadr args))))

(map car args) で変数名を、(map cadr args) で初期値を取り出します。関数 cadr が必要になるので、ついでに cdar, caar, cddr も定義しておきます。 あとはラムダ式を組み立て、それに初期値を渡せばいいわけです。とても簡単ですね。名前付き let はあとから作ります。

簡単な実行例を示します。

>>> (define a 0)
Compile => (ldc 0 def a stop)
Value => a

>>> (define b 1)
Compile => (ldc 1 def b stop)
Value => b

>>> (let ((a 10) (b 20)) (cons a b))
Compile => (ldc 10 ldc 20 args 2 ldf (ld (0 . 0) ld (0 . 1) args 2 ldg cons app
rtn) app stop)
Value => (10 . 20)

>>> a
Compile => (ldg a stop)
Value => 0

>>> b
Compile => (ldg b stop)
Value => 1

Gauche の場合、let はシンタックス形式ですが、このようにマクロでも let を実装することができます。

●and と or

次は and と or を作ります。

リスト ; and と or

(define-macro and
  (lambda args
    (if (null? args)
        #t
      (if (null? (cdr args))
          (car args)
        `(if ,(car args) (and ,@(cdr args)) #f)))))

(define-macro or
  (lambda args
    (if (null? args)
        #f
      (if (null? (cdr args))
          (car args)
        `(let ((+value+ ,(car args)))
          (if +value+ +value+ (or ,@(cdr args))))))))

and は拙作のページ マクロ (1) で作成したプログラムと同じです。or は引数の評価結果を局所変数 +value+ に格納し、それが真ならば +value+ を返すようにします。変数捕捉を避けるため、独習 Scheme 三週間 にならって変数名に + を付けています。gensym を使いたい方は、プログラムを改造してみてください。

それでは実行例を示します。

>>> (and 1 2 3)
Compile => (ldc 1 sel (ldc 2 sel (ldc 3 join) (ldc #f join) join) (ldc #f join)
stop)
Value => 3

>>> (and 1 #f 3)
Compile => (ldc 1 sel (ldc #f sel (ldc 3 join) (ldc #f join) join) (ldc #f join)
 stop)
Value => #f

>>> (or 1 2 3)
Compile => (ldc 1 args 1 ldf (ld (0 . 0) sel (ld (0 . 0) join) (ldc 2 args 1 ldf
 (ld (0 . 0) sel (ld (0 . 0) join) (ldc 3 join) rtn) app join) rtn) app stop)
Value => 1

>>> (or #f #f 3)
Compile => (ldc #f args 1 ldf (ld (0 . 0) sel (ld (0 . 0) join) (ldc #f args 1 l
df (ld (0 . 0) sel (ld (0 . 0) join) (ldc 3 join) rtn) app join) rtn) app stop)
Value => 3

>>> (or #f #f #f)
Compile => (ldc #f args 1 ldf (ld (0 . 0) sel (ld (0 . 0) join) (ldc #f args 1 l
df (ld (0 . 0) sel (ld (0 . 0) join) (ldc #f join) rtn) app join) rtn) app stop)

Value => #f

●let*

次は let* を作ります。let* は let を入れ子にすることで実現することができます。次のリストを見てください。

リスト : let*

(define-macro let*
  (lambda (args . body) 
    (if (null? (cdr args))
        `(let (,(car args)) ,@body)
      `(let (,(car args)) (let* ,(cdr args) ,@body)))))

args に要素が 1 個しかない場合は、それを let に変換します。複数ある場合は、先頭の要素を let に変換し、let の本体でマクロ let* を再帰呼び出しします。これで let* を入れ子の let に変換することができます。

簡単な実行例を示します。

>>> (let* ((a 100) (b a) (c (cons a b))) c)
Compile => (ldc 100 args 1 ldf (ld (0 . 0) args 1 ldf (ld (1 . 0) ld (0 . 0) arg
s 2 ldg cons app args 1 ldf (ld (0 . 0) rtn) app rtn) app rtn) app stop)
Value => (100 . 100)

●letrec

次は letrec を作ります。letrec は定義する変数を初期値の中で参照することができます。let ではこれを実現することはできません。let は最初に初期値を評価しますが、このとき環境には定義する変数がまだ存在していないためエラーになるのです。そこで、letrec は変数を *undef* で初期化してから、あらためて set! で初期値を代入することにします。つまり、次のように変換します。

(letrec ((a expr-a) (b expr-b) ...) body ...)
                   │
                   ↓ 
(let ((a '*undef*) (b '*undef*) ...)
  (set! a expr-a)
  (set! b expr-b)
  ...
  body
  ... )

プログラムは次のようになります。

リスト : letrec

;;; 2 つのリストを受け取る map 関数
(define map-2
  (lambda (fn xs ys)
    (if (null? xs)
        '()
      (cons (fn (car xs) (car ys)) (map-2 fn (cdr xs) (cdr ys))))))

(define-macro letrec
  (lambda (args . body)
    (let ((vars (map car args))
          (vals (map cadr args)))
      `(let ,(map (lambda (x) `(,x '*undef*)) vars)
            ,@(map-2 (lambda (x y) `(set! ,x ,y)) vars vals)
            ,@body))))

args から map で変数名を取り出して vars に、初期値を取り出して vals にセットします。次に、let で局所変数を定義して *undef* で初期化します。そして、その変数に set! で初期値を代入します。Scheme の map は複数のリストを受け取ることができますが、前回作成した map は 1 つのリストしか受け取ることができません。そこで、2 つのリストを受け取る関数 map-2 を定義しました。

簡単な例として、リストを反転する関数 reverse を letrec で書き直します。次のリストを見てください。

リスト : リストの反転

(define reverse
  (lambda (ls)
    (letrec ((iter (lambda (ls a)
                     (if (null? ls)
                         a
                       (iter (cdr ls) (cons (car ls) a))))))
      (iter ls '()))))

letrec を使うことで、iter を再帰呼び出しすることができます。簡単な実行例を示しましょう。

>>> (reverse '(a b c d e))
Compile => (ldc (a b c d e) args 1 ldg reverse app stop)
Value => (e d c b a)

>>> (reverse '())
Compile => (ldc () args 1 ldg reverse app stop)
Value => ()

ただし、このプログラムには問題があります。次の例を見てください。

>>> (letrec ((a a)) a)
Compile => (ldc *undef* args 1 ldf (ld (0 . 0) lset (0 . 0) pop ld (0 . 0) rtn)
app stop)
Value => *undef*

この場合、Gauche ではエラーになります。この問題は Structure and Interpretation of Computer Programs (SICP) 4.1.6 Internal Definitions で説明されています。興味のある方は SICP を参考に、プログラムを改造してみてください。

●名前付き let

次は名前付き let を作ります。これは letrec に変換すると簡単です。

(let name ((a init-a) (b init-b) ...) body ...)
                   │
                   ↓
(letrec ((name (lambda (a b ...) body ...)))
  (name init-a init-b ...))

プログラムは次のようになります。

リスト : 名前付き let

(define-macro let
  (lambda (args . body)
    (if (pair? args)
        `((lambda ,(map car args) ,@body) ,@(map cadr args))
      ;; named-let
      `(letrec ((,args (lambda ,(map car (car body)) ,@(cdr body))))
        (,args ,@(map cadr (car body)))))))

args の先頭要素がリストでなければ名前付き let と判断します。args が名前で、body の先頭要素が変数と初期値を格納したリストになります。あとは letrec でラムダ式を定義して、それに初期値を渡して呼び出すだけです。

簡単な例として、名前付き let で reverse を作ります。プログラムリストと実行結果を示します。

リスト : リストの反転

(define reversei
  (lambda (ls)
    (let loop ((ls ls) (a '()))
      (if (null? ls)
          a
          (loop (cdr ls) (cons (car ls) a))))))
>>> (reversei '(a b c d e))
Compile => (ldc (a b c d e) args 1 ldg reversei app stop)
Value => (e d c b a)

>>> (reversei '())
Compile => (ldc () args 1 ldg reversei app stop)
Value => ()

●begin

次は begin を作ります。begin は引数なしのラムダ式に変換するだけです。プログラムリストと実行結果を示します。

リスト : begin

(define-macro begin
  (lambda args
    (if (null? args)
        `((lambda () '*undef*))
      `((lambda () ,@args)))))
>>> (begin)
Compile => (args 0 ldf (ldc *undef* rtn) app stop)
Value => *undef*

>>> (begin 1 2 3 4 5)
Compile => (args 0 ldf (ldc 1 pop ldc 2 pop ldc 3 pop ldc 4 pop ldc 5 rtn) app s
top)
Value => 5

●cond

次は cond を作ります。再帰定義を使うと cond も簡単に定義することができます。次のリストを見てください。

リスト : cond

(define-macro cond
  (lambda args
    (if (null? args)
        '*undef*
      (if (eq? (caar args) 'else)
          `(begin ,@(cdar args))
        (if (null? (cdar args))
            `(let ((+value+ ,(caar args)))
              (if +value+ +value+ (cond ,@(cdr args))))
          `(if ,(caar args)
               (begin ,@(cdar args))
            (cond ,@(cdr args))))))))

args が空リストの場合は *undef* を返します。条件部が else の場合は、残りの S 式を無条件に実行します。これは begin を使えば簡単です。そうでなければ、条件部を評価します。このとき、条件部しかない場合は評価結果を +value+ にセットし、真であればその値を返します。S 式がある場合は、begin で S 式を順番に評価します。条件部が偽の場合は、次の節をチェックします。これは cond を再帰呼び出しするだけです。

cond の簡単なテストプログラムと実行結果を示します。

リスト : cond のテスト

(define cond-test
  (lambda (x)
    (cond ((eq? x 'a) 1)
          ((eq? x 'b) 2)
          ((eq? x 'c) 3)
          (else 0))))
>>> (cond-test 'a)
Compile => (ldc a args 1 ldg cond-test app stop)
Value => 1

>>> (cond-test 'b)
Compile => (ldc b args 1 ldg cond-test app stop)
Value => 2

>>> (cond-test 'c)
Compile => (ldc c args 1 ldg cond-test app stop)
Value => 3

>>> (cond-test 'd)
Compile => (ldc d args 1 ldg cond-test app stop)
Value => 0

>>> (cond-test 'e)
Compile => (ldc e args 1 ldg cond-test app stop)
Value => 0

●case

次は case を作ります。case も再帰定義を使うと簡単です。

リスト : case

;;; 探索
(define memv
  (lambda (x ls)
    (if (null? ls)
        #f
        (if (eqv? x (car ls))
            ls
          (memv x (cdr ls))))))

(define-macro case
  (lambda (key . args)
    (if (null? args)
        '*undef*
      (if (eq? (caar args) 'else)
          `(begin ,@(cdar args))
        `(if (memv ,key ',(caar args))
             (begin ,@(cdar args))
           (case ,key ,@(cdr args)))))))

キーを探索するため、関数 memv を定義します。memv は要素の比較に eqv? を使います。case の引数 args が空リストの場合は *undef* を返します。節の先頭要素が else の場合は、begin で残りの要素を順番に評価します。そうでなければ、memv で key を探索します。見つけたら残りの S 式を begin で順番に評価します。見つからない場合は次の節をチェックします。これは case を再帰呼び出しするだけです。

簡単な case のテストプログラムと実行結果を示します。

リスト : case のテスト

(define case-test
  (lambda (x)
    (case x
      ((a b c) 1)
      ((d e f) 2)
      ((g h i) 3)
      (else    0))))
>>> (case-test 'a)
Compile => (ldc a args 1 ldg case-test app stop)
Value => 1

>>> (case-test 'e)
Compile => (ldc e args 1 ldg case-test app stop)
Value => 2

>>> (case-test 'i)
Compile => (ldc i args 1 ldg case-test app stop)
Value => 3

>>> (case-test 'j)
Compile => (ldc j args 1 ldg case-test app stop)
Value => 0

●do

最後に do を作りましょう。do は letrec に変換すると簡単です。次のリストを見てください。

リスト : do

(define-macro do
  (lambda (var-form test-form . args)
    (let ((vars (map car var-form))
          (vals (map cadr var-form))
          (step (map cddr var-form)))
      `(letrec ((loop (lambda ,vars
                        (if ,(car test-form)
                            (begin ,@(cdr test-form))
                          (begin
                            ,@args
                            (loop ,@(map-2 (lambda (x y)
                                             (if (null? x) y (car x)))
                                           step
                                           vars)))))))
        (loop ,@vals)))))

まず引数 var-form から変数、初期値、更新式 (step-form) を取り出して vars, vals, step にセットします。step の要素は、step-form が存在しない場合は空リストになることに注意してください。あとは letrec で lambda 式を組み立てます。test-form の先頭要素を評価して、その結果が真であれば begin で残りの要素を評価して返します。

そうでなければ、begin で do の本体を評価して loop を再帰呼び出します。ここで、step-form がない場合、ラムダ式の引数 x は空リストになるので、対応する変数 y をそのまま渡します。そうでなければ、(car x) で step-form を取り出して渡します。これで繰り返しを実現することができます。

簡単な例として do を使って reverse をプログラムします。リストと実行結果を示します。

リスト : do のテスト

(define reverse-do
  (lambda (xs)
    (do ((ls xs (cdr ls)) (result '()))
        ((null? ls) result)
      (set! result (cons (car ls) result)))))
>>> (reverse-do '(a b c d e))
Compile => (ldc (a b c d e) args 1 ldg reverse-do app stop)
Value => (e d c b a)

今回はここまでです。次回は「継続」の実装に挑戦してみましょう。


●プログラムリスト1

;;;
;;; secd.scm : SECD 仮想マシンによる Scheme コンパイラ (R7RS-small 対応版)
;;;
;;;            (1) 基本機能の実装
;;;            (2) 伝統的なマクロの実装
;;;
;;;            Copyright (C) 2009-2021 Makoto Hiroi
;;;
(import (scheme base) (scheme cxr) (scheme write) (scheme read)
        (scheme file) (scheme process-context))

;;; データの追加
(define-syntax push!
  (syntax-rules ()
    ((_ place x) (set! place (cons x place)))))

;;; データの取得
(define-syntax pop!
  (syntax-rules ()
    ((_ place)
     (let ((x (car place)))
       (set! place (cdr place))
       x))))

;;; ドットリストの生成
(define (list* . xs)
  (if (null? (cdr xs))
      (car xs)
      (cons (car xs) (apply list* (cdr xs)))))

;;; 変数の位置を求める
(define (position-var sym ls)
  (let loop ((i 0) (ls ls))
    (cond ((null? ls) #f)
          ((symbol? ls)
           (if (eq? sym ls) (- (+ i 1)) #f))
          ((eq? sym (car ls)) i)
          (else
           (loop (+ i 1) (cdr ls))))))

;;; フレームと変数の位置を求める
(define (location sym ls)
  (let loop ((i 0) (ls ls))
    (if (null? ls)
        #f
      (let ((j (position-var sym (car ls))))
        (if j
            (cons i j)
          (loop (+ i 1) (cdr ls)))))))

;;; 自己評価フォームか
(define (self-evaluation? expr)
  (and (not (pair? expr)) (not (symbol? expr))))

;;; マクロか
(define (macro? expr)
  (let ((val (assoc expr *global-environment*)))
    (and val (pair? (cdr val)) (eq? 'macro (cadr val)))))

;;; マクロのコードを取り出す
(define (get-macro-code expr)
  (caddr (get-gvar expr)))

;;; S 式をコンパイルする
(define (compile expr)
  (comp expr '() '(stop)))

(define (comp expr env code)
  (cond ((self-evaluation? expr)             ; 自己評価フォーム
         (list* 'ldc expr code))
        ((symbol? expr)                      ; 変数
         (let ((pos (location expr env)))
           (if pos
               ;; 局所変数
               (list* 'ld pos code)
             ;; 大域変数
             (list* 'ldg expr code))))
        ((eq? (car expr) 'quote)
         (list* 'ldc (cadr expr) code))
        ((eq? (car expr) 'if)
         (let ((t-clause (comp (caddr expr) env '(join)))
               (f-clause
                 (if (null? (cdddr expr))
                     (list 'ldc '*undef 'join)
                   (comp (cadddr expr) env '(join)))))
           (comp (cadr expr) env (list* 'sel t-clause f-clause code))))
        ((eq? (car expr) 'lambda)
         (let ((body (comp-body (cddr expr) (cons (cadr expr) env) '(rtn))))
           (list* 'ldf body code)))
        ((eq? (car expr) 'define)
         (comp (caddr expr) env (list* 'def (cadr expr) code)))
        ((eq? (car expr) 'define-macro)
         (comp (caddr expr) env (list* 'defm (cadr expr) code)))
        ((eq? (car expr) 'set!)
         (let ((pos (location (cadr expr) env)))
           (if pos
               ;; 局所変数
               (comp (caddr expr) env (list* 'lset pos code))
             ;; 大域変数
             (comp (caddr expr) env (list* 'gset (cadr expr) code)))))
        ((macro? (car expr))
         ;; マクロ展開してからコンパイルする
         (let ((new-expr (vm '()
                             (list (cdr expr))
                             (get-macro-code (car expr))
                             (list (list '() '() '(stop))))))
           (comp new-expr env code)))
        (else  ; 関数呼び出し
         (complis (cdr expr)
                  env
                  (list* 'args
                         (length (cdr expr))
                         (comp (car expr) env (cons 'app code)))))))

;;; body のコンパイル
(define (comp-body body env code)
  (if (null? (cdr body))
      (comp (car body) env code)
    (comp (car body)
          env
          (list* 'pop
                 (comp-body (cdr body) env code)))))

;;; 引数を評価するコードを生成
(define (complis expr env code)
  (if (null? expr)
      code
    (comp (car expr) env (complis (cdr expr) env code))))

;;;
;;; 仮想マシン
;;;

;;; ls の先頭から n 個の要素を取り除く
(define (drop ls n)
  (if (zero? n)
      ls
    (drop (cdr ls) (- n 1))))

;;; 局所変数の値を求める
(define (get-lvar e i j)
  (if (<= 0 j)
      (list-ref (list-ref e i) j)
    (drop (list-ref e i) (- (+ j 1)))))

;;; 局所変数の値を更新する
(define (set-lvar! e i j val)
  (if (<= 0 j)
      (set-car! (drop (list-ref e i) j) val)
    (if (= j -1)
        (set-car! (drop e i) val)
      (set-cdr! (drop (list-ref e i) (- (+ j 2))) val))))

;;; 大域変数の値を求める
(define (get-gvar sym)
  (let ((val (assoc sym *global-environment*)))
    (if val
        (cdr val)
      (error "unbound variable " sym))))

;;; 大域変数の値を更新する
(define (set-gvar! sym val)
  (let ((cell (assoc sym *global-environment*)))
    (if cell
        (set-cdr! cell val)
      (error "unbound variable " sym))))

;;; 仮想マシンでコードを実行する
(define (vm s e c d)
  (case (pop! c)
    ((ld)
     (let ((pos (car c)))
       (vm (cons (get-lvar e (car pos) (cdr pos)) s) e (cdr c) d)))
    ((ldc)
     (vm (cons (car c) s) e (cdr c) d))
    ((ldg)
     (vm (cons (get-gvar (car c)) s) e (cdr c) d))
    ((ldf)
     (vm (cons (list 'closure (car c) e) s) e (cdr c) d))
    ((lset)
     (let ((pos (car c)))
       (set-lvar! e (car pos) (cdr pos) (car s))
       (vm s e (cdr c) d)))
    ((gset)
     (set-gvar! (car c) (car s))
     (vm s e (cdr c) d))
    ((app)
     (let ((clo (car s)) (lvar (cadr s)))
       (if (eq? (car clo) 'primitive)
           (vm (cons (apply (cadr clo) lvar) (cddr s)) e c d)
         (vm '() (cons lvar (caddr clo)) (cadr clo) (cons (list (cddr s) e c) d)))))
    ((rtn)
     (let ((save (car d)))
       (vm (cons (car s) (car save)) (cadr save) (caddr save) (cdr d))))
    ((sel)
     (let ((t-clause (car c))
           (e-clause (cadr c)))
       (if (car s)
           (vm (cdr s) e t-clause (cons (cddr c) d))
         (vm (cdr s) e e-clause (cons (cddr c) d)))))
    ((join)
     (vm s e (car d) (cdr d)))
    ((pop)
     (vm (cdr s) e c d))
    ((args)
     (let loop ((n (car c)) (a '()))
       (if (zero? n)
           (vm (cons a s) e (cdr c) d)
         (loop (- n 1) (cons (pop! s) a)))))
    ((def)
     (let ((sym (car c)))
       (push! *global-environment* (cons sym (car s)))
       (vm (cons sym (cdr s)) e (cdr c) d)))
    ((defm)
     (let ((sym (car c)))
       (push! *global-environment*
              (cons sym (cons 'macro (car s))))
       (vm (cons sym (cdr s)) e (cdr c) d)))
    ((stop) (car s))
    (else (error "unknown opcode"))))

;;; 大域変数
(define *global-environment*
        (list
          (list 'car   'primitive car)
          (list 'cdr   'primitive cdr)
          (list 'cons  'primitive cons)
          (list 'eq?   'primitive eq?)
          (list 'eqv?  'primitive eqv?)
          (list 'pair? 'primitive pair?)
        ))

;;; read-eval-print-loop
(define (repl)
  (let loop ()
    (display "\n>>> ")
    (guard (err
            (else (display "ERROR: ")
                  (display (error-object-message err))
                  (unless
                   (null? (error-object-irritants err))
                   (display (car (error-object-irritants err))))
                  (newline)))
           (let* ((expr (compile (read)))
                  (v (vm '() '() expr '())))
             (when (eof-object? v) (exit))
             (display "Compile => ")
             (display expr)
             (newline)
             (display "Value => ")
             (display v)
             (newline)))
    (loop)))

;;; ファイルの読み込み
(for-each
  (lambda (name)
    (with-input-from-file name
      (lambda ()
        (let loop ()
          (let ((output (vm '() '() (compile (read)) '())))
            (display output)
            (newline)
            (if (not (eof-object? output))
                (loop)
                #f))))))
  (cdr (command-line)))

;;; 実行
(repl)

●プログラムリスト2

;;;
;;; mlib.scm : micro Scheme 用ライブラリ
;;;
;;;            Copyright (C) 2009-2021 Makoto Hiroi
;;;

;;; 述語
(define null? (lambda (x) (eq? x '())))
(define not (lambda (x) (if (eq? x #f) #t #f)))

;;; cxxr
(define cadr (lambda (x) (car (cdr x))))
(define cdar (lambda (x) (cdr (car x))))
(define caar (lambda (x) (car (car x))))
(define cddr (lambda (x) (cdr (cdr x))))

;;; cxxxr
(define cadar (lambda (x) (car (cdr (car x)))))

;;;
;;; リスト操作関数
;;;
(define list (lambda args args))

(define append
  (lambda (xs ys)
    (if (null? xs)
        ys
      (cons (car xs) (append (cdr xs) ys)))))

;;;
;;; リストの探索
;;;
(define memq
  (lambda (x ls)
    (if (null? ls)
        #f
        (if (eq? x (car ls))
            ls
          (memq x (cdr ls))))))

(define memv
  (lambda (x ls)
    (if (null? ls)
        #f
        (if (eqv? x (car ls))
            ls
          (memv x (cdr ls))))))

;;; 連想リストの探索
(define assq
  (lambda (x ls)
    (if (null? ls)
        #f
      (if (eq? x (car (car ls)))
          (car ls)
        (assq x (cdr ls))))))

(define assv
  (lambda (x ls)
    (if (null? ls)
        #f
      (if (eqv? x (car (car ls)))
          (car ls)
        (assv x (cdr ls))))))

;;;
;;; 高階関数
;;;

;;; マッピング
(define map
  (lambda (fn ls)
    (if (null? ls)
        '()
      (cons (fn (car ls)) (map fn (cdr ls))))))

(define map-2
  (lambda (fn xs ys)
    (if (null? xs)
        '()
      (cons (fn (car xs) (car ys)) (map-2 fn (cdr xs) (cdr ys))))))

;;; フィルター
(define filter
  (lambda (fn ls)
    (if (null? ls)
        '()
      (if (fn (car ls))
          (cons (car ls) (filter fn (cdr ls)))
        (filter fn (cdr ls))))))

;;; 畳み込み
(define fold-right
  (lambda (fn a ls)
    (if (null? ls)
        a
      (fn (car ls) (fold-right fn a (cdr ls))))))

(define fold-left
  (lambda (fn a ls)
    (if (null? ls)
        a
      (fold-left fn (fn a (car ls)) (cdr ls)))))

;;;
;;; マクロ
;;;

;;; quasiquote
(define transfer
  (lambda (ls)
    (if (pair? ls)
        (if (pair? (car ls))
            (if (eq? (caar ls) 'unquote)
                (list 'cons (cadar ls) (transfer (cdr ls)))
              (if (eq? (caar ls) 'unquote-splicing)
                  (list 'append (cadar ls) (transfer (cdr ls)))
                (list 'cons (transfer (car ls)) (transfer (cdr ls)))))
          (list 'cons (list 'quote (car ls)) (transfer (cdr ls))))
      (list 'quote ls))))

(define-macro quasiquote (lambda (x) (transfer x)))

;;; let (named-let)
(define-macro let
  (lambda (args . body)
    (if (pair? args)
        `((lambda ,(map car args) ,@body) ,@(map cadr args))
      ; named-let
      `(letrec ((,args (lambda ,(map car (car body)) ,@(cdr body))))
        (,args ,@(map cadr (car body)))))))

;;; and
(define-macro and
  (lambda args
    (if (null? args)
        #t
      (if (null? (cdr args))
          (car args)
        `(if ,(car args) (and ,@(cdr args)) #f)))))

;;; or
(define-macro or
  (lambda args
    (if (null? args)
        #f
      (if (null? (cdr args))
          (car args)
        `(let ((+value+ ,(car args)))
          (if +value+ +value+ (or ,@(cdr args))))))))

;;; let*
(define-macro let*
  (lambda (args . body)
    (if (null? (cdr args))
        `(let (,(car args)) ,@body)
      `(let (,(car args)) (let* ,(cdr args) ,@body)))))

;;; letrec
(define-macro letrec
  (lambda (args . body)
    (let ((vars (map car args))
          (vals (map cadr args)))
      `(let ,(map (lambda (x) `(,x '*undef*)) vars)
            ,@(map-2 (lambda (x y) `(set! ,x ,y)) vars vals)
            ,@body))))

;;; begin
(define-macro begin
  (lambda args
    (if (null? args)
        `((lambda () '*undef*))
      `((lambda () ,@args)))))

;;; cond
(define-macro cond
  (lambda args
    (if (null? args)
        '*undef*
      (if (eq? (caar args) 'else)
          `(begin ,@(cdar args))
        (if (null? (cdar args))
            (caar args)
          `(if ,(caar args)
               (begin ,@(cdar args))
            (cond ,@(cdr args))))))))

;;; case
(define-macro case
  (lambda (key . args)
    (if (null? args)
        '*undef*
      (if (eq? (caar args) 'else)
          `(begin ,@(cdar args))
        `(if (memv ,key ',(caar args))
             (begin ,@(cdar args))
           (case ,key ,@(cdr args)))))))

;;; do
(define-macro do
  (lambda (var-form test-form . args)
    (let ((vars (map car var-form))
          (vals (map cadr var-form))
          (step (map cddr var-form)))
      `(letrec ((loop (lambda ,vars
                        (if ,(car test-form)
                            (begin ,@(cdr test-form))
                          (begin
                            ,@args
                            (loop ,@(map-2 (lambda (x y)
                                             (if (null? x) y (car x)))
                                           step
                                           vars)))))))
        (loop ,@vals)))))

;;;
;;; マクロを使った関数の定義
;;;

;;; reverse
(define reverse
  (lambda (ls)
    (letrec ((iter (lambda (ls a)
                     (if (null? ls)
                         a
                       (iter (cdr ls) (cons (car ls) a))))))
      (iter ls '()))))

;;; reverse (named-let 版)
(define reversei
  (lambda (ls)
    (let loop ((ls ls) (a '()))
      (if (null? ls)
          a
          (loop (cdr ls) (cons (car ls) a))))))

初版 2009 年 9 月 19 日
改訂 2021 年 6 月 12 日

Copyright (C) 2009-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Scheme | NextPage ]