えー、例によって大変長いこと放置しておりましてすみません。
今回公開する部分の実装自体は2月頃に出来ていたのですが、 その後、何かとナニな状態が続きまして、ずっと放置していました。
――放置するにもほどがある、ってもんですが。
今回の修正点は以下の通りです。
GLOBALをかましたソースは こちらから参照可能です。
ダウンロードは、UNIX版がこちら、 Windows版がこちら。
Javaと同様、ラベル付きのbreakやcontinueが使えるようになりました。
label: for (i = 0; i < 10; i++) { for (j = 0; j < 10; j++) { print(" i.." + i + ", j.." + j + "\n"); if (j == 5) { break label; # 外側のループを脱出 } } }
実装方法ですが、crowbarではブロックや式の階層に合わせて 再帰呼び出しを行なっており、従来から、breakやcontinueがあった時には、 文を評価した際の戻り値(StatementResult構造体)によりちまちまと 状態を返していました(こちらを参照)。 今回は、StatementResult構造体にラベルを持たせ、 ラベルが一致するところまで遡るように修正したわけです。
typedef struct { StatementResultType type; union { CRB_Value return_value; char *label; } u; } StatementResult;
今までやってなかったのが単なる手抜きなのであって、 それ以上のものではないです。
イテレータに関しては、 ビルトインスクリプトの仕掛けを作った時点で簡単に実装できることは わかっていたので、今回実装しました。仕様はGoF流です。 現時点では、イテレータを取得できるのは配列だけです。
a = {1, 2, 3, 4, 5, 6}; # イテレータを使ったループ for (ite = a.iterator(); !ite.is_done(); ite.next()) { v = ite.current_item(); print("(" + v + ")"); }
配列のiterator()メソッド※1 を呼ぶと、イテレータが取得できます。
イテレータには、以下のメソッドがあります。
Javaに慣れた人は、この仕様に違和感を憶えるかもしれません。
Javaのイテレータは、配列などの要素と要素の間を指します。 next()を呼ぶと、イテレータは次の「要素と要素の間」に移動し、 その時乗り越えた要素を返します。
それに対し、crowbarのイテレータ(GoF流)は、要素を直接指します。 よって、current_item()で現在の要素を取得できますし、 next()を呼ばない限り、current_item()は何度呼んでも同じ要素を返します。
どちらの仕様がよいかについてはいろいろ意見がありそうですが、 要素を「見る」だけでイテレータが進んでしまうJavaの仕様は、 私には使いづらいと思えたのでcrowbarはGoF流の仕様になっています。
なお、GoF流のイテレータでは、 要素がひとつもない時や、最後の要素を指してからさらにnext()を呼んだ時には イテレータが「範囲外」を指すことになります。 その状態ではis_done()メソッドがtrueを返しますが、 もしis_done()をチェックせずにcurrent_item()を呼べば、 現状のcrowbarの配列のイテレータではあっさり配列の範囲外エラーを出します。
配列のイテレータの実装は以下の通りです。 __create_array_iterator()はイテレータ生成用の隠し関数であり、 ビルトインスクリプトの中に記述されています。 配列のiterator()メソッド(もどき)は、 この関数を呼び出して取得したイテレータを返しているだけです。
function __create_array_iterator(array) { this = new_object(); index = 0; this.first = closure() { index = 0; }; this.next = closure() { index++; }; this.is_done = closure() { return index >= array.size(); }; this.current_item = closure() { return array[index]; }; return this; }
イテレータができたので、foreachも作りました。 実は最初は、 crowbarにはクロージャがあるのでforeachはライブラリで提供できるから、 言語レベルで取り込む必要はないかな、くらいに思っていたのですが、 やっぱりbreakやcontinueがfor文と同じように使えないと不便ですから、 構文として実装しました。
crowbarのforeachは以下のように使います。
a = {1, 2, 3, 4, 5, 6}; foreach(v : a) { print("(" + v + ")"); }
foreachの構文も言語によっていろいろあって、 たとえばPerlはこうです。
foreach $a (@collection) { # $aを使った処理 }
Javaでは、foreach文ではなくfor文の拡張とされており、以下のように書きます。
for (Object a : collection) { // aを使った処理 }
C#ではこうです。
foreach (Object a in collection) { // aを使った処理 }
crowbarの仕様は、JavaとC#のちゃんぽんになっています。 なぜこうしたかといえば、 Javaの仕様では「foreach的なループ」を言い表しにくい(たとえば 開発メンバ同士話をしているとき、 「そこはforeachで回しといて」と言えない)ですし、 C#の仕様では、「in」が予約語になってしまうため プログラムを書く時に不便だと思えるからです。
従来、crowbarのネイティブ関数内で配列やassocなどを確保した時には、 GCで解放されないようにするため、 その参照をスタックに積む必要がありました (こちらを参照)。 この仕様は、ネイティブ関数を書く人にとって負担になりますし、 何よりも「GCがどのようなタイミングで動くのか」という実装を ネイティブ関数の作者に晒すことになります。 これは具合が悪いので、 ネイティブ関数内で確保されたオブジェクトについては、 処理系が面倒を見ることにしました。
具体的な実装方法ですが、まず、 ネイティブ関数内で確保したオブジェクトへの参照を、 CRB_LocalEnvironmentで保持できるようにしました(crowbar.h)。
/* ネイティブ関数内で確保されたオブジェクトへの参照を * 連結リストで保持する。 */ typedef struct RefInNativeMethod_tag { CRB_Object *object; struct RefInNativeMethod_tag *next; } RefInNativeMethod; /* そのリストを、CRB_LocalEnvironment中に保持する。 */ struct CRB_LocalEnvironment_tag { char *current_function_name; int caller_line_number; CRB_Object *variable; /* ScopeChain */ GlobalVariableRef *global_variable; RefInNativeMethod *ref_in_native_method; struct CRB_LocalEnvironment_tag *next; };
そして、CRB_dev.hにてネイティブ関数に公開している関数のうち、 オブジェクトの確保を伴うものについて、確保したオブジェクトを カレントのCRB_LocalEnvironmentに登録するようにしました。 なお、オブジェクト生成の関数はeval.cなどcrowbarの内部でも使いますが、 こちらは登録する必要がないため、 「内部向け」と「ネイティブ関数向け」とでインタフェースを分けました。
/* 配列のオブジェクトを確保するネイティブ関数向けの * インタフェース。 */ CRB_Object * CRB_create_array(CRB_Interpreter *inter, CRB_LocalEnvironment *env, int size) { CRB_Object *ret; /* crb_create_array_i()は内部向けの関数 */ ret = crb_create_array_i(inter, size); /* CRB_LocalEnvironmentに参照を格納 */ add_ref_in_native_method(env, ret); return ret; }
その上で、GCを修正してCRB_LocalEnvironmentのref_in_native_methodが 指す先もmarkするようにすれば、ネイティブ関数は、 自分が確保したオブジェクトについて、 勝手に解放されることを心配する必要がなくなるわけです。 もちろん、ネイティブ関数を抜けた時点でCRB_LocalEnvironment は解放されますから、余計なゴミが残る心配はありません。
この仕様の問題点としては、「ネイティブ関数内ですごい回数ループして、 ループ内でオブジェクトを作っては捨てるようなケースでは、 関数を抜けるまでオブジェクトが解放されない」という点が挙げられます。 しかし、どうせネイティブ関数内でしか使わないオブジェクトなら、 なにも面倒なcrowbarのオブジェクトを使わずとも Cでmalloc()していればいいわけですから、 現実問題そんなコードは滅多にないだろう、と考えてこのような仕様にしました JNIでもそうなっているようですし。
crowbarは、エラーメッセージの日本語文字列をerror_message.c の中に埋め込んでいます。これを外部ファイルにしていないのは、 実行形式単体で動作させるためですし、そもそもエラーメッセージを 英語ではなく日本語で書いているのは、私が英語が苦手なためです。 いやその無理に英語で書いても、 メッセージがぞんざいになるのは目に見えていますし(言い訳にもなってないな)。
しかし、WindowsなどShift-JISの環境で、Cの文字列リテラル中に 「表」や「ソ」などの文字を埋め込むと、gccでは問題が発生します。 文字列リテラル中では「\」は特別な意味を持ちますが、 「表」の2バイト目である0x5cがまさに「\」の文字コードだからです (この問題に関する解説は こちら。
そのため、0.4.01までのcrowbarでは、エラーメッセージに「メソッド」 と書くべきところでは「method」と、 「正規表現」と書くべきところでは「regular expression」 と書くことでしのいでいました。 gccだけを相手にすればよいのなら「正規表\現」と書けばよいのですが、 この問題はgccがちゃんと日本語対応していないことにあるのであって、 そんなものに特別に対応したコードとすることに抵抗があったためです。
――が、考えてみれば、現在配布しているWindows向けのMakefileには 堂々とgccと書いてありますし、-Wallなんてオプションも付けていますから、 今更そんなこと気にするのも変か、ということで、本バージョンからは Windows版のerror_message.cはgcc専用になっています。 すみませんがgcc以外のコンパイラを使っている人は、 「error_message_not_gcc.c」を使ってください。
前回のToDoリストからすると、こういうことになりそうですが。
現在仕事の方がナニな状態で時間がかけられないこと、 その他いろいろあって、今後の予定はちょっと立ちません。
いずれにしても気長にお待ちくださいませ。