nptのドキュメントです。
参照元:ANSI Common Lisp npt
前へ:hold変数の使い方
次へ:関数の登録
脱出関数とは、脱出が行われる可能性のある関数のことです。
以前、脱出関数とはerrorが生じる可能性のある関数だと説明しました。
しかし脱出とはerrorだけを意味するものではありません。
脱出とは、現在の処理を中断し、スタックフレームを 強制的に終わらせる命令のことを意味します。
脱出の要因となる命令は、次の5つ存在します。
tagbody / goblock / return-fromcatch / throwhandler-caserestart-caseつまり、実行の流れを大きく変更し、 別のスタックフレームにジャンプすることを脱出と呼びます。
脱出はC言語のsetjmp/longjmp、C++言語のtry/catchのようなものです。
しかしnptではこれらの機能を使わず、 ただ脱出関数を終了させることで脱出を実現しています。
脱出関数とは何らかの技術のことを言っているのではなく、 C言語で関数を作成するときの決まり事のことです。
これから説明する決まり事を守ることで、 Common Lispの動作を実現できるようになります。
例文を用いて脱出関数の作成を説明します。
int test(void)
{
lisp_format8_(NULL, "Start~%", NULL);
lisp_funcall8_(NULL, "TEST-THROW", NULL);
lisp_format8_(NULL, "End~%", NULL);
return 0;
}脱出関数の名前はアンダーバー_で終わるので、 例文にあるlisp_format8_とlisp_funcall8_は脱出関数です。
test関数では、脱出関数の戻り値を全て無視していますが、 本来であれば正しく処理する必要があります。
正しい脱出関数に書き換えたものを下記に示します。
int test_(void)
{
if (lisp_format8_(NULL, "Start~%", NULL))
return 1;
if (lisp_funcall8_(NULL, "TEST-THROW", NULL))
return 1;
if (lisp_format8_(NULL, "End~%", NULL))
return 1;
return 0;
}それぞれの脱出関数が値を返却した場合に return 1を実行することで、 正しい脱出関数test_を作成できました。
ここで、test-throw関数が次のように定義されているとします。
(defun test-throw ()
(throw 'hello 999))test_がlisp_funcall8_を実行して test-throwを呼び出したとき、 対応するcatchが存在するならばthrowが実施されるので、 lisp_funcall8_関数は1を返却します。
するとtest_関数もそのままreturn 1が実行されるため、 次のformat文は実行されずにtest_関数が終了します。
このようにして脱出が実現されます。
lisp_push_controlがある場合通常の関数test内でlisp_push_controlが使用されているとします。
int test(void)
{
addr control, v;
lisp_push_control(&control);
v = Lisp_hold();
lisp_format8_(NULL, "Start~%", NULL);
lisp_funcall8_(v, "TEST-THROW", NULL);
lisp_format8_(NULL, "End: ~A~%", v, NULL);
lisp_pop_control_(control);
return 0;
}脱出関数に書き換える際、push / popで囲まれている文は、 もし脱出が生じた場合でもすぐにreturn 1を実行するのではなく、 必ずpopしてスタックフレームを解放する必要があります。
例えば次のように変更します。
int test_(void)
{
addr control, v;
lisp_push_control(&control);
v = Lisp_hold();
if (lisp_format8_(NULL, "Start~%", NULL))
goto escape;
if (lisp_funcall8_(v, "TEST-THROW", NULL))
goto escape;
if (lisp_format8_(NULL, "End: ~A~%", v, NULL))
goto escape;
escape:
return lisp_pop_control_(control);
}例文のように単純な場合はgotoを用いても問題ありませんが、 複雑な構文になると分かりづらくなってしまいます。
そこで、nptの開発では次のような書き換えを行っていました。
static int test_call_(void)
{
addr v;
v = Lisp_hold();
if (lisp_format8_(NULL, "Start~%", NULL))
return 1;
if (lisp_funcall8_(v, "TEST-THROW", NULL))
return 1;
if (lisp_format8_(NULL, "End: ~A~%", v, NULL))
return 1;
return 0;
}
int test_(void)
{
addr control;
lisp_push_control(&control);
(void)test_call_();
return lisp_pop_control_(control);
}さらに例文では、脱出の判定を行う際に
if (...)
return 1;という文を多用しますが、 この構文は非常に多くの場所で使用するため、 下記のようなマクロを作成します。
#define Return(x) {if (x) return 1;}このマクロを用いることで、test_call_関数は次のように書き換えることができます。
static int test_call_(void)
{
addr v;
v = Lisp_hold();
Return(lisp_format8_(NULL, "Start~%", NULL));
Return(lisp_funcall8_(v, "TEST-THROW", NULL));
Return(lisp_format8_(NULL, "End: ~A~%", v, NULL));
return 0;
}脱出関数を多く作成する予定があるのであれば、 この単純なマクロが非常に使いやすいので 使用を検討してみるのも良いかと思います。
まとめますと、脱出関数は次のような規則を課したものを言います。
_で終わる(必須ではない)int1、通常は0前章(hold変数の使い方)では、 階乗を出力する例文を作りました。
しかし説明の都合上エラー処理を行っていなかったため、 脱出関数の戻り値をすべて無視していました。
正しい脱出関数に書き換えたものを下記に示します。
static int fact_(addr x, addr value)
{
addr control, y;
if (! lisp_plus_p(value)) {
lisp_fixnum(x, 1);
return 0;
}
lisp_push_control(&control);
y = Lisp_hold();
if (lisp_funcall8_(y, "1-", value, NULL))
goto escape;
if (fact_(y, y))
goto escape;
if (lisp_funcall8_(x, "*", value, y, NULL))
goto escape;
escape:
return lisp_pop_control_(control);
}
int main_lisp(void *ignore)
{
addr control, x, y;
lisp_push_control(&control);
x = Lisp_hold();
y = Lisp_hold();
lisp_fixnum(y, 123);
if (fact_(x, y))
goto escape;
if (lisp_format8_(NULL, "fact: ~A! = ~A~%", y, x, NULL))
goto escape;
escape:
return lisp_pop_control_(control);
}例文のfact_関数はこれ以上修正の必要が無い完成されたものになります。