« Paul Graham の新しい Lisp 系言語 Arc がリリース | メイン | Arc 開発環境 on Emacs (Meadow) »

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

トラックバック

このエントリーのトラックバックURL:
http://d3.jpn.org/mt/mt-tb.cgi/97

コメントを投稿