AM-Scheme(1)
あの方からパーティの案内状が届いた。何でも、今度新宿と六本木ヒルズで大々的にやるから いっらっしゃいと言う案内である。
何度か行った事があり、その時の住所録に記録が残っていたようだ。また、行っても いいんだけど、あれは麻薬だからなあ。中毒になっちゃうよ。
名前は、ACLとか言うらしい。FreeBSDの5だったかの時、「うまく動きませんよ」でも、4用の libmだったかを入れたら動きましたとアメリカの人に報告したら、それ以来覚えられてしまった みたい。今は、ACL8だけど、FreeBSD(7|8)では、何もしなくても動くのかな?
JREだと、compat7を入れてあげないと、FreeBSD8では動かないよと、宣伝されてたけど、 どうなんだろう? 8が出たら、ACL8も試してみるかな。
CPU実験の元
ネットをうろうろしてたら、とーだいのCPU実験の古い資料が見つかった。。
こちらが全体像になってて、レイトレの 解説まで載ってた。昔の実験は、camelじゃなくて、scheme 大好き先生の持ち授業だったのね。
コンパイラの作り方も 公開されてる。わずか10こまで、解説しちゃうなんて凄いよ。CPSの話が面白い。
補足資料として何かないかと調べてみたら 継続渡し形式(CPS)が見つかった。 shiroさんとこへのリンクも載ってた。 東大で授業を受けてる気分になれるなぁ。
授業は授業でいいんだけど、自習も。 という事で、この前から始めたAM-Schemeの ソース解読を淡々と進めてみよう。まずは、全体像という事で、*.h に目を通す。
config.h
config.h を見る。
/* * Here is System declaration */ /* #define THINK_C /* THINK C version 4.0 for Macintosh */ /* #define THINK_C_DA /* THINK C version 4.0 for Macintosh DA */ /* #define MPW /* Macintosh Programmer's Workshop */ /* #define BSD /* 4.x BSD */ /* #define SYSV /* System V */ /* #define VAXC /* VAX/VMS -- defined by CC automagically (by Akira Kida) */ /* #define MSDOS /* MS-DOS + MS-C6 (by Akira Kida) */
システムによる場合分けをしてますね。Macintosh を初めて使った時は、ターボパスカルだった なあ。それから、光速Cになって、最後は、THINK C を使った。だって、MPWなんて、高くて 買えなかったもの。
そうそう、AM-Schemeを、FreeBSD 7.2 へ持ってきてコンパイルしたら、strlen と mallocが 未定義でっせと警告された。しょうがないので、string.h と stdlib.h をincludeして 黙らせた。FreeBSDと言えども、ちらちらと仕様が変わっているのね。(FreeBSD 6.4 では 未警告だったよ)
amscm.h
amscmの重要な定義が凝縮されている。
S式を格納する時に付けるマーカーと言うかTAGの定義 T_XXX (T_BOXって、どんなやつ? 想像出来ないや)
続いて、実際の格納形式とそれらに簡便にアクセスするためのマクロ類。たとえば、下記の ような具合。(左側の数字は、行番号)
50 /* cons cell structure */
51 struct _cons_cell {
52 cell_type _type;
53 pointer _car;
54 pointer _cdr;
55 };
56 typedef struct _cons_cell *cons_cell;
57 #define CONSSIZE (long)(sizeof(struct _cons_cell))
58 #define ispair(p) (type(p) == T_PAIR)
59 #define car(p) (((cons_cell)(p))->_car)
60 #define cdr(p) (((cons_cell)(p))->_cdr)
61 #define isclosure(p) (type(p) == T_CLOSURE)
62 #define ismacro(p) (type(p) == T_MACRO)
63 #define closure_code(p) car(p)
64 #define closure_env(p) cdr(p)
closureは、cons_cellに格納してるんだと、読める。同じような具合で、continuationは vector_cellに格納してると分かる。
後半部分は、よく使うマクロ類(stackへのpush/popとか)が定義されている。
prim.h
組み込み手続きの extern 宣言をまとめて書き出してある。これらの実体は prim.cにほとんどまとめられている。
op.h
Eval_Cycleという評価器が使うdispatch用のTAG OP_XXX や、数学関係のTAG、後は、readerが使う token のTAG類が定義されている。
と、まあ、大急ぎで概観してきた。後は、debuggerでも使って、追いかけてみるかな。 まずは、起動してプロンプトが出てくるまでの動きをみてみよう。
動き出すまで
[sakae@nil ~/v110/src]$ gdb amscm
GNU gdb 6.1.1 [FreeBSD]
:
(gdb) b main
Breakpoint 1 at 0x804b860: file generic_os.c, line 168.
(gdb) run
Starting program: /usr/home/sakae/v110/src/amscm
Breakpoint 1, main (argc=Error accessing memory address 0x2: Bad address.) at generic_os.c:168
168 {
(gdb) n
main (argc=1, argv=0xbfbfec44) at generic_os.c:172
172 while (--argc) {
(gdb)
186 printf(Banner);
(gdb)
Hello, This is AM-Scheme Interpreter Version 1.10
187 if (init_scheme())
main関数は、generic_os.c にて定義されてるんだな。n で、少し進めると、Banner が 出てきて、初期化の部分に差し掛かった。ここで、ソースが概観してみると、 この後、初期化ファイルを読み込んでから、(若し起動時に指定した)引数があれば それをscmファイルと見なして、読み込み。REPLとおぼしきRep_From_Inputを呼び出している。 すると、初期化は、init_scheme()という事になる。
(gdb) s init_scheme () at init.c:314 314 NIL = (pointer)&_NIL;
init.cにて、初期化してるんだ。ソースを概観する。
314 NIL = (pointer)&_NIL; 315 for (i = 0 ; i < HASHTABLESIZE ; i++) oblist[i] = (pointer)&_NIL; 316 T = (pointer)&_T; F = (pointer)&_F; eof_object = (pointer)&_eof_object;
NILとかT,F,eof_objectと言う、基本データを決定。また、schemeのシンボル名の登録先を 初期化してるんだな。 続いて、こまごました変数類を初期化が続き
343 if ((Heap1 = (char *)malloc(MALLOCCAST(Heap_Size))) == NULL)
344 goto INIT_FAILED;
345 if ((long)Heap1 <= (long)OP_MAXNUM) {
346 char *new = (char *)malloc(MALLOCCAST(Heap_Size));
347 free(Heap1);
348 if (new == NULL) goto INIT_FAILED;
349 Heap1 = new;
350 }
Schemeのデータを格納する領域をOSから貰ってきてるな。データの格納領域だから、さしずめ、倉庫と 言った所か。この後、同一サイズの倉庫をもう一つ確保してる。(Heap1とHeap2)
359 Current_Heap = Heap1; 360 sprintf(&strbuff[0], 361 "allocated 2*%ld bytes main memory areas\n", Heap_Size); 362 PrintString(std_output, &strbuff[0]); 363 364 size = sizeof(pointer)*Stack_Size; 365 stackbottom = (pointer *)malloc(MALLOCCAST(size));
Heap1を現在の倉庫として登録後、誇らしげにメッセージを出してから、 今度は、演算の時に使うスタックエリアを確保、それ類に関する変数を初期化。 S式の読み込み時に使うスタックも確保。そして、終盤へ。
378 /* initialize Scheme's globals */ 379 init_globals(); 380 381 envir = global_env; 382 code = NIL; 383 Is_read_complete = 0; 384 385 Init_read_stack() 386 387 #ifdef USE_SIGNAL 388 signal(SIGINT, get_signal); 389 #endif 390 car(global_env) = cons(cons(RESET_PATCH, 391 mk_closure(cons(NIL, NIL), global_env)), 392 car(global_env)); 393 394 scheme_alive = 1; 395 return (0);
init_globals()は、後回しにして、envir,codeと言う評価器用のレジスターを初期化。後は、 割り込みを登録してから、環境を整えて終了。
それでは、後回しにした init_globals()を見る。
98 /* initialize NIL */
99 type(NIL) = 0;
100 car(NIL) = cdr(NIL) = NIL;
101 /* init T */
102 type(T) = 0;;
103 car(T) = cdr(T) = T;
104 /* init F */
105 type(F) = 0;
106 car(F) = cdr(F) = F;
107 /* init eof_object */
108 type(eof_object) = 0;
109 car(eof_object) = cdr(eof_object) = eof_object;
110 /* init global_env */
111 global_env = cons(NIL, NIL);
112
113 /* initialize character table */
114 for (i = 0 ; i < CHARMAX ; i++) {
115 x = (pointer)&char_table[i];
116 type(x) = T_CHAR;
117 cvalue(x) = (uShort)i;
118 }
119
120 /* initialize syntax */
121 mk_syntax(OP_LAMBDA, "lambda");
122 mk_syntax(OP_QUOTE, "quote");
:
136 mk_syntax(OP_CASE0, "case");
Schemeレベルの構造を作っていく。最初に基本定数(?)、次は環境だけど、今は何も ないので、NILで埋めている。続いて(256)文字分。次は、syntaxと呼ばれる、評価上特別 扱いしなければならないシンボル。OP_LAMBDAと言うのは、評価器内の実行開始位置(ラベル) で、文字列"lambda"は、ユーザーが書く記述名。
138 /* initialize primitive procedure */ 139 mk_prim1(OP_PRIMEVAL, "eval", MUST_BE_N(1)); 140 mk_prim1(OP_PRIMAPPLY, "apply", AT_LEAST_N(2)); 141 mk_prim1(OP_CONTINUATION, "call-with-current-continuation", MUST_BE_N(1)); 142 mk_prim1(OP_LOAD, "load", MUST_BE_N(1)); 143 mk_prim1(OP_READ, "read", AT_LEAST_N(0)); 144 mk_prim1(OP_RCHR, "read-char", AT_LEAST_N(0)); 145 mk_prim1(OP_PKCH, "peek-char", AT_LEAST_N(0)); 146 147 mk_prim2(PR_LIST, "list", AT_LEAST_N(0)); 148 mk_prim2(PR_CAR, "car", MUST_BE_N(1)); : 287 mk_prim2(PR_SETBOX, "set-box!", MUST_BE_N(2)); 288 mk_prim2(PR_EXIT, "exit", MUST_BE_N(0));
続いてたくさんある組み込み手続きの登録、OP_XXXで始まるものは、評価器内で直接評価 される関数。PR_YYYで始まるものは、個別に登録されてる関数だ。個別の方の実体は、関数への ポインターになっている。
mk_prim1(2)の第二引数は、ユーザーが書く名前。第三引数は、関数が必要とする引数の数 (アリティー)だ。MUST_BE_N(n)は、正確にn個の引数が必要という意味。AT_LEAST_N(n) は、n個以上の引数が必要と言う意味だ。
そんじゃ、ちと、登録関数mk_prim2 を見てみる
80 x = mk_sym_from_cstr(name, STDCHARCASE); 81 y = (pointer)Get_Heap(PRIMITIVESIZE(strlen(name))); 82 strcpy(primname(y), name); 83 type(y) = T_PRIM2; 84 primargs(y) = n; 85 primproc(y) = proc; 86 car(global_env) = cons(cons(x, y), car(global_env));
Get_Heapで、必要なサイズをSchemeが管理する領域(倉庫)から切り出してきて、構造体を埋めている。 最後に、グローバルな環境に登録してる。おっと、mk_sym_from_cstrを見ておく。この関数の 実体は、mk_symになるので、これを見る。
196 /* fisrt check oblist */
197 hashpos = hash(name, len);
198 for (x = oblist[hashpos]; x != NIL ; x = cdr(x)) {
199 if (str_eqv(name, len, symname(car(x)), symlen(car(x))))
200 break;
201 }
202
203 if (x != NIL) return (car(x));
204 else {
205 char *new;
206 x = (pointer)Get_Heap(SYMBOLSIZE(len));
207 for (i = 0, new = symname(x) ; i < len ; i++) new[i] = name[i];
208 new[len] = '\0';
209 symlen(x) = len;
210 type(x) = T_SYMBOL;
211 symprop(x) = NIL;
212 g_push(&x)
213 oblist[hashpos] = cons(x, oblist[hashpos]);
214 g_pop(1)
関数名(namae)とその長さ(len)から、hash値を求め、そこから、調べるべき名前の登録簿の 位置を割り出し、既に登録ずみか確認する。登録ずみなら、その位置を返す。まだ未登録なら 名前を登録するエリアを切り出し、シンボルとして構造体を作る。後は、登録簿に、追加する。 Cで書くと、リンクの付け替えとかになるんだろうけど、consを1発発行するだけで済んじゃう のは、楽でいいなあ。
ついでだから、Get_Heapはどうなってるか、見ておくか。
31 len = ((size + ALIGNMENT - 1)/ALIGNMENT)*ALIGNMENT;
32 if (Free_Heap + len > Heap_Size) {
33 gc();
34 if (Free_Heap + len > Heap_Size)
35 Error("run out of heap area --- rerurn to top level");
36 }
37 new = &Current_Heap[Free_Heap];
38 Free_Heap += len;
39 return (new);
要求されたサイズをアライメント単位で切り上げてから、そのサイズが提供出来るか確認。 だめなら、ごみ掃除(gc)をやって場所の確保に努める。大丈夫なら、空きエリアの場所を返す。(その分だけ、 空きエリアが減っていく)
gc
最後に、gc を見ておく。実際の動きはこんな具合だ。gc-verboseで、ごみ掃除の状況を報告せよ、 と指定。dustと言うのは、ごみ製造手続きだ。これを動かすだけで、見えないごみが、わんさかと 生まれてくれる。(注: 特に意識しなくても、手続きの実行に伴い、ごみは生まれます。今回は 、顕著にごみが生まれるようにしました。)
> (gc-verbose #t)
#t
> (define (dust d)
(dust (cons #t d)))
dust
> (dust '())
gc...done. 167424 bytes are free.
gc...done. 125568 bytes are free.
gc...done. 94176 bytes are free.
gc...done. 70656 bytes are free.
Error: Unbound variable d
[return to toplevel]
ごみ製造機を動かしてみた。計算の進行により、データが蓄積されて行く為、ごみ掃除を やっても、だんだんと使える領域が少なくなって行き、最後は、自分自身の居場所も無くなって 、プログラムが停止されました。(この挙動、ちと怪しいな)
どういう仕組みなってるかだな。予想は付くけど、STOP_COPY法だ。 SICPに解説がある。
67 if (gc_verbose) PrintString(std_output, "gc...");
68
69 Old_Heap = Current_Heap;
70 Current_Heap = (Old_Heap == Heap1) ? Heap2 : Heap1;
71 Free_Heap = 0;
72
73 /* realloc system globals */
74 for (i = 0 ; i < HASHTABLESIZE ; i++)
75 oblist[i] = re_allocation(oblist[i]);
76 global_env = re_allocation(global_env);
:
101 for (i = 0 ; i < output_ports ; i++)
102 o_port_list[i] = re_allocation(o_port_list[i]);
103
104 if (gc_verbose) {
105 sprintf(&strbuff[0], "done. %ld bytes are free.\n",
106 Heap_Size - Free_Heap);
107 PrintString(std_output, &strbuff[0]);
108 }
データを格納する倉庫を2つ用意し、一つは常用でどんどんデータを詰めていく。やがて一杯に なると、もう一方を常用に切り替える。切り替えて古くなった倉庫から、使われている ものだけを、(切り替えられた)常用倉庫に移してあげるって言う戦略。
問題は、どれが使われているかをどうやって見つけるかだ。Scheme-systemが知っている管理情報 を元に、移動して行く事になる。この情報の事をrootと呼んでいる。
どんな物がrootになっているかと言うと、まずは、登録簿に登録されてる物、 システムが使っている環境、、、いろいろあって最後は、出力port。 実際の移動作業は、re_allocation 関数が受け持っている。(アート引越しセンターみたいなもんだな)この関数の一部を参考に見てみる。
161 case T_SYNTAX: 162 x = syntaxprop(old); 163 v = syntaxlen(old); 164 new = (pointer)Get_Heap(SYMBOLSIZE(v)); 165 memcpy(new, old, SYMBOLSIZE(v)); 166 type(old) = T_NULL; 167 new_alloc(old) = new; 168 syntaxprop(new) = re_allocation(x); 169 return (new);
移したい荷物はoldというエリアの物。荷物の種類によって大きさ等が違うので、場合分けしてる。 必要なデータを新しい方に写し、移したよマーク(SICPよれば、失恋マークと言うそうだ)として、 T_NULLを書き込んでおく。また、転居先も、古い方に書いておく。ここでは示さなかったけど、 このルーチンの頭の所で、 転居したか調べていて、転居してる事が判明したら、即転居先を返すようになっている。
荷物がチェーン状に繋がっている可能性があるので、自分自身を再帰的に 呼び出して、移動を完了させる。終了したら、新しい移動先を返す。
こういう仕組みで、倉庫を切り替えながら使っている。これを称して和田先生は、「式年遍宮に よるGC」と言っておられた。
大分長くなったので、この辺で。