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」と言っておられた。
大分長くなったので、この辺で。