Arc のソースを読む #1
Arc のソースをつらつらと眺めているので、ついでにメモを書いてみる。チュートリアルと Arc Cross Reference も適宜参照してもらうとよいかも。
スタートアップファイル (as.scm)
まず、Arc を起動するときに最初に読み込む Scheme のスタートアップファイル as.scm から。(as.scm の as は Arc Startup の略?)
(require mzscheme) ; promise we won't redefine mzscheme bindings (load "ac.scm") (require "brackets.scm") (use-bracket-readtable) (aload "arc.arc") (aload "libs.arc") (tl)
いくつかのファイルが順にロードされていて、どうやら以下のように分かれているようだ。
ac.scm (ac は Arc Core の略?)は Arc の式を Scheme に変換して実行するコア部分。Arc から Scheme に変換するコンパイラと、プリミティブな関数が Scheme にて書かれている。
brackets.scm は Scheme のリードテーブルを拡張して [+ _ 1] のような式を扱えるようにしている("[" と "]" というトークンはもともと Scheme にはないため)。
arc.arc で様々な関数/マクロが Arc にて記述されている。
libs.arc では HTTP サーバやアプリケーションサーバ等のライブラリをロードしている。
read-eval-print ループ (ac.scm)
次に ac.scm (Arc Core?)。まず as.scm の最後で呼ばれている (tl) を見てみる。tl は Top Level の略か。
(define (tl)
(display "Use (quit) to quit, (tl) to return here after an interrupt.\n")
(tl2))
(define (tl2)
(display "arc> ")
(on-err (lambda (c)
(set! last-condition* c)
(display "Error: ")
(write (exn-message c))
(newline)
(tl2))
(lambda ()
(let ((expr (read)))
(if (eqv? expr ':a)
'done
(let ((val (arc-eval expr)))
(write (ac-denil val))
(namespace-set-variable-value! '_that val)
(namespace-set-variable-value! '_thatexpr expr)
(newline)
(tl2)))))))
これは典型的な read-eval-print loop (repl)。on-err の部分はエラー処理で、エラーが起きたらそっちに飛んで、最後にまた tl2 を呼びだしている。
read は Scheme のをそのまま使っている(前述の "[" と "]" を扱うためにリードテーブルはいじってある)。Arc も Scheme も S 式なので楽できるね :D
eval は当然 Arc から Scheme に変換しないと評価できないので arc-eval という関数でやってる。arc-eval については後述。
で、評価結果を write で表示。ここで、ac-denil って関数でラップしてるんだけど、この ac-denil や ac-niltree 等の関数は他の部分でも沢山ある。Arc をも含めた一般的な Lisp では、真偽値は t と nil で空リストも nil で表わすことになっている一方、Scheme だと真偽値は #t と #f で、空リストは '() で表わすことになっている。で、その辺の差異を ac-denil 等の関数を使って吸収してると思っておけば OK。
コードを見るとわかるけど、Arc のプロンプトで :a と入力すると Scheme に戻ってこれる。で再度 Arc に入るには (tl)。
eval/コンパイル (ac.scm)
次は arc-eval。arc-eval は単に Arc の式(expr)を ac という関数で Scheme の式に変換して Scheme の eval に渡しているだけ。つまり ac が Arc→Scheme のコンパイラになっている。ac は Arc Compile(r) の略?
(define (arc-eval expr)
(eval (ac expr '()) (interaction-environment)))
(define (ac s env)
(cond ((string? s) (string-copy s)) ; to avoid immutable strings
((literal? s) s)
((eqv? s 'nil) (list 'quote 'nil))
((ssyntax? s) (ac (expand-ssyntax s) env))
((symbol? s) (ac-var-ref s env))
((ssyntax? (xcar s)) (ac (cons (expand-ssyntax (car s)) (cdr s)) env))
((eq? (xcar s) 'quote) (list 'quote (ac-niltree (cadr s))))
((eq? (xcar s) 'quasiquote) (ac-qq (cadr s) env))
((eq? (xcar s) 'if) (ac-if (cdr s) env))
((eq? (xcar s) 'fn) (ac-fn (cadr s) (cddr s) env))
((eq? (xcar s) 'set) (ac-set (cdr s) env))
; this line could be removed without changing semantics
((eq? (xcar (xcar s)) 'compose) (ac (decompose (cdar s) (cdr s)) env))
((pair? s) (ac-call (car s) (cdr s) env))
(#t (err "Bad object in expression" s))))
で ac は Shiro Kawai さんの 2008/01/30 の日記にある通り、
よく、「Schemeはdefine, quote, if, lambda, set!の基本構文があれば残りの構文はそれで書ける」と言うけれど、ほんとにそれをやっちゃった。コアで定義されている構文は quote, quasiquote, if, fn (lambdaに相当), set (defineとset!に相当)だけ。
数値等のリテラルならそのまま。シンボルなら ac-var-ref で値を返す。シンタックスなら expand-ssyntax で展開して再度コンパイル。式がペアなら関数呼び出し。で、あとは quote, quasiquote, if, fn, set をそれぞれ専用の関数(ac-*)で処理してる。
シンプルな Lisp ですね :D
もし、Arc→Scheme の変換結果を見てみたければ、以下のように arc-eval をちょっと変更してやって、Scheme のコードを表示させるようにしてみるとよいかも。
#|
(define (arc-eval expr)
(eval (ac expr '()) (interaction-environment)))
|#
(define (arc-eval expr)
(let ((scm (ac expr '())))
(display "scm: ")
(newline)
(pretty-print scm)
(eval scm (interaction-environment))))
実行してみるとこんな感じ。
arc> 1
scm:
1
1
arc> (+ 1 2)
scm:
(ar-funcall2 _+ 1 2)
3
arc> (= a 1)
scm:
((lambda ()
'nil
(begin (let ((| a| 1)) (namespace-set-variable-value! '_a | a|) | a|))))
1
arc> a
scm:
_a
1
arc> (def fact (n)
(if (<= n 1) 1 (* n (fact (- n 1))))))
scm:
((lambda ()
'nil
(ar-funcall3 _sref _sig '(n . nil) 'fact)
((lambda ()
'nil
(if (not (ar-false? (ar-funcall1 _bound 'fact)))
((lambda ()
'nil
(ar-funcall1 _disp "*** redefining ")
(ar-funcall1 _disp 'fact)
(ar-funcall1 _writec #\newline)))
'nil)
(begin
(let ((| fact|
(lambda (n)
'nil
(if (not (ar-false? (ar-funcall2 _<= n 1)))
1
(ar-funcall2
_*
n
(ar-funcall1 _fact (ar-funcall2 _- n 1)))))))
(namespace-set-variable-value! '_fact | fact|)
| fact|))))))
#<procedure: fact>
arc> (fact 5)
scm:
(ar-funcall1 _fact 5)
120
とりあえず今日はここまで。次回は、個別のコンパイル関数(ac-fn等)やマクロ関連を見ていくつもり :D