トップ «前の日記(2014-02-15) 最新 次の日記(2014-02-19)» 編集

Route 477

過去の日記


2014-02-17

[biwascheme] オリンピックイヤーなのでBiwaScheme 0.6.2をリリースしました

いやオリンピック関係ないんですけど、気づいたら前回のリリースが2011年12月で、そっからほぼずっと放置してたんですが、未だにPRやIssue報告もらったりするので、せめてそれ取り込むくらいはするか、そうでなければきっぱりと終了宣言をすべきだなぁと思って、とりあえず現時点のものをリリースしました。

「プルリクを取り込むくらいはする」ためにはmasterがクリーンな状態、つまりいつでもリリースできる状態になっている必要があります。今回はmasterで特定のデモが動かない状態になっていて、その調査を中途半端に進めたメモだけ手元にあるという状態で、このようなシチュエーションで放置するとリリースにこぎつけるのが大変になるという教訓です。*1

スタックトレース

0.6.2の目玉機能はエラー時にバックトレースが出るようになったことです。これは僕の実装ではなく、#9でコミットしてもらったものです。

仕組みについてはdoc/以下の資料 に書いたので、せっかくなので日本語でも解説を書いておきます。

バックトレースそのものはInterpreter#call_traceに入っています。これは文字列(関数名)の配列です。関数名をどこから取ってくるかですが、op_refer-globalの実行時にInterpreter#last_referに名前をセットするようになっています。Compilerの仕様で、ローカル変数はILへのコンパイル時に無名になってしまう(番号で参照するようになる)ため、ローカル関数の場合は名前が取れません。このときはバックトレースには"(anon)"と表示されます。*2

スタックトレースとTCO

で、あとはop_applyでcall_traceに関数名をpushし、op_returnでpopしてやればいい…のですが、BiwaSchemeはTCO(末尾呼び出しの最適化)を行うので、op_applyの回数とop_returnの回数が一致しないケースがあるのですよね。

この差分を数えるために、Intepreter#tco_counterという変数が用意されています。これは数値の配列で、op_frameで0をpushし、末尾呼び出しが起きたときにインクリメントし、op_returnでpopするようになっています。末尾呼び出しかどうかを判定するために、op_tco_hinted_applyという命令が増えています。*3

あとはop_returnでcall_traceをpopするときに、tco_counterに記録されている数だけ余計にpopしてやれば回数が正しく揃うという寸法です。

スタックトレースのサイズを制限する

さてこれで記録はできるようになったのですが、一つ問題があって、スタックトレースが無限に伸びてしまうケースがあるのです。
Schemeでは末尾呼び出しを最適化することが言語仕様で規定されているため*4、末尾再帰を使って無限ループを書くというイディオムがあります。例えばこんな。

(let loop ((x 1))
  (do_something)
  (sleep 1)
  (loop))

JSで無限ループなんか書いたらブラウザが固まるんじゃね、と思われたかも知れませんが、BiwaSchemeには同期的にsleepを書けるという機能があって、実際にこの落ちゲーのデモでもmain関数でこのようなループを行っています。
実装的には、(sleep 1)すると「1秒後にプログラムの実行を再開する」というsetTimeoutが走るようになっています。JS上に処理系を実装するからこそできる荒業ですね。*5

話が逸れましたが、そういう事情でバックトレースのサイズはデフォルトで40個に制限していて、それを超えると古いものから捨てられます。とりあえずこれでメモリを際限なく使うことはなくなりました。
ただその代わり、この方法だと不便なケースがもしかしたらあるかも知れません。以下のプログラムでfの呼び出しでエラーが発生したとき、そこまでのバックトレースが出てほしいのですが、big_taskが関数呼び出しを40段以上行って、正常にreturnしたとすると、40個のcall_traceはすべてpop()されてしまいます。そのためfのエラーのバックトレースには何も表示されないことになります。

    ...
    (big_task)
    (f 1)

いまのところ、これに対する対処は特に考えていません (実際に問題になってから考える予定)。とりあえずBiwaScheme.max_trace_sizeか、Interpreter#max_trace_sizeに大きな数値を設定することで回避してください。

追記(2014/02/18)

例えばTCOの存在下では「ソースコードの字面に対応するスタックトレース」は そもそも存在しないわけで、それを取れるようにしようと頑張るのは間違いだと思います。 (そこにスタックトレースを期待している時点で、プログラマの頭の中のモデルが 違っています)。 むしろSchemeのモデルなら、チェックポイントの通過履歴を取っておく、 というような概念の方が良いかもしれません。

[http://practical-scheme.net/wiliki/wiliki.cgi?Gauche%3A%E3%83%87%E3%83%90%E3%83%83%E3%82%ACより引用]

*1 ちなみにこの他に「作業状況をメモっておく」「内部構造などはなるべくコメントやドキュメントを書く」「テストが通る状態にしておく」という条件があり、これらはそこそこ満たせていると思う

*2 ローカル変数が無名になるのはオリジナルである3imp.pdfでそうなっています

*3 tco_counterの長さはスタックフレーム長に一致するので、本来はフレーム情報に含めるのが自然かも

*4 ソースは「そうだったような気がする」

*5 将来的にはみんなGeneratorで同期sleepするようになるんですかねぇ


過去の日記