大島さんの記事 http://d.hatena.ne.jp/squeaker/20061031#p2 を読みながら、pepsi & coke の勉強をしています。
環境の準備
idst-5.7 に付属する jolt を使って勉強します。必要なファイルは http://piumarta.com/pepsi/ にあります。ここでは、ソースとバイナリ両方使います。ソースだけ落としてコンパイルしても勿論大丈夫です。まずバイナリを / 以下にコピーします。windows の場合は cygwin が必要です。ソースで使うのは example/jolt ディレクトリだけなので、それを適当な場所にコピーして make しておきます。以後このディレクトリで作業を行います。なお、linux で使うためには以下を実行する必要があります。
$ execstack -s main
シェルを起動して coke 式を評価
coke の実装である jolt のシェルを起動して式を入力すると答えが返ります。基本的な計算のテストをします。ここではカーソルキー等、一般的な readline のキー操作が使えます。終了するには ctrl + D です。
$ ./main.exe boot.k - .(+ 3 4) => 7 .(+ 3 (* 2 3)) => 9
coke では lisp のような形式 (関数名 引数 ...) でリストを評価します。引数を全て評価してから関数が呼ばれます。すると => の後に答えが表示されます。大島さんのテキストでは、ここから [] を使ったメッセージ送信について解説がありますが、難しいのでもう少し () だけを使った実験をします。関数定義の define を実験してみます。scheme とよく似ています。
.(define plus (lambda (a b) (+ a b))) => 10797800 .(plus 3 4) => 7 .(define fib (lambda (n) . (if (== n 0) 1 . (if (== n 1) 1 . (+ (fib (- n 1)) (fib (- n 2))))))) => 10823464 .(fib 10) => 89
関数定義の時にも数字が出てきて鬱陶しいですが、これは関数のアドレスです。アドレスで直接呼び出しても同じです。
.(define minus (lambda (a b) (- a b))) => 10857176 .(10857176 100 10) => 90
という事は結局 coke のプログラムは整数を括弧で包んだ物と言えます。
C の関数を呼ぶ
boot.k を見ると、printf が定義されています。
.(printf "hello world\n") hello world => 12 .(printf "3 + 4 = %i\n" (+ 3 4)) 3 + 4 = 7 => 10
(_dlsym _RTLD_DEFAULT 関数名) で任意の C の関数を coke の関数に出来るようです。芸が無いですが、putc を定義してみます。
.(define puts (_dlsym _RTLD_DEFAULT "puts")) => -519487552 .(puts "hage") hage => 10
メッセージ送信
book.k では、C の関数以外に pepsi の StdOut オブジェクトもインポートしています。pepsi とは、coke の為に用意された別の言語です。ややこしいですが、coke のソースは pepsi で書かれています。インポートされた StdOut オブジェクトを使うには send 関数を使います。send 関数を使うと、(send メッセージ オブジェクト 引数 ...) の形で pepsi オブジェクトにメッセージを送信出来ます。??? なんだこれは??? boot.k を見ると、StdOut がオブジェクトとして定義されているのでこれを使ってみます。test.k がにサンプルがあります。
.(send 'nextPutAll: StdOut '"hello world\n") hello world => 25
' をつける奴とつけない奴に注意が必要です。StdOut 以外には ' を付ける必要があります。 を使うとこの send 式をもう少し読みやすく表現する事が出来ます。 式では、要素の順番が変わってメッセージの ' を省略します。引数の ' は省略出来ないので注意が必要です。
.[StdOut nextPutAll: '"hello world\n"] hello world => 25
ここで ' についてもうちょっと調べます。なにも付けない 3 は機械語の世界の 3 ですが、' を付けると pepsi オブジェクトになります。pepsi オブジェクトの整数の内部表現は、Squeak VM に習って 数字 * 2 + 1 になっているので coke シェルは 7 と答えます。
.3 => 3 .(quote 3) => 7 .'3 => 7 .[StdOut nextPutAll: ['3 printString]] 3 => 3
StdOut も printf と同じく define 文で定義された物ですが、C の関数ではなく pepsi のオブジェクトです。両者の定義方法を比較してみます。
- (define printf (_dlsym _RTLD_DEFAULT "printf"))
- C の関数
- (define StdOut (import "StdOut"))
- pepsi のオブジェクト
文法を定義する。
さていよいよ coke の本来の目的である文法を定義します。大島さんの例は難しいのでもっと簡単に、単に数字の一だけ返す構文と、数字を二倍にする構文を作ります。今度は長いので、mySyntax.k という名前でファイルを保存します。
; mySyntax.k (syntax one (lambda (node compiler) (let () '1))) (syntax double (lambda (node compiler) (let () `(* ,[node at: '1] 2)))) (define Compiler (import "Compiler")) [Compiler parseOption: '"-vs"]
syntax 文は文法を定義するのに使います。たとえば最初のやつは (one) と言う単純な文です。文法と関数がどう違うかと言うと、関数は引数を実行してから呼ばれますが、文法は引数を実行する前に呼ばれます。lambda に使われる node とcompiler は、この文が呼ばれる時の構文木とコンパイラの状態を示します。どちらも pepsi オブジェクトなので、操作する際には [ ] を使います。最後のリストを返すのに使う妙なクォートは、クアジクオートと言って、全体ではクォートするけど途中一部 , のところだけクォートから外す機能です。
syntax の動作を決めるには三つの方法があります。
- 最初の例のように値を返す。lambda で与えられた compile オブジェクトを使って、引数をこの時点で評価する事も出来ます。
- 二番目の例のようにリストを作り直して返す。リストはさらに他の部分で評価されます。
- VPU を直接操作する。この例では無いですが、バーチャルマシンを手動で操作する事も出来ます。
最後の二行は無くても大丈夫ですが、コンパイルしてる様子を表示するオプションです。これをテストしてみます。
$ ./main.exe boot.k mySyntax.k - .(one) SYNTAX -> (#one) REWRITE => 1 => 1 .(one 123 muchakucha) SYNTAX -> (#one 123 #muchakucha) REWRITE => 1 => 1 .(double 100) SYNTAX -> (#double 100) REWRITE => (#* 100 2) => 200
one の引数としてめちゃくちゃ書いてもとにかく 1 が返るというのが味噌です。今回は行き着きませんでしたが、さらにパーサーの部分をいじると、こんな括弧コッカだらけじゃなくて javascript が書けたり python が書けたりするみたいです。