SECDV Schemeの実装 - port入出力


撃墜率高すぎ。。https://github.com/okuoku/yuni/issues
SECDVでR7RSライブラリを実装するついでに同じテストをR6RS/R7RS処理系でも走らせているけど、意外と問題が出る。yuniにはR6RSで使うためのR7RS語彙がいくつか実装されていて、そのライブラリの不具合であったり、処理系自体の不具合(や微妙な挙動)であったり様々。
というわけで、テストを足すたびに特定の処理系を外す処理をいつも入れている気がする。一応、yuniはR6RS/R7RS準拠を謳う処理系をターゲットにしているが、それでもこれだけの差が出るのが多様性の難しいところというか。。
Vicareのboolean=?のバグのように軽微なものはすぐ報告( https://github.com/marcomaggi/vicare/issues/97 )できるけど、まず一旦yuniライブラリを使わない元の処理系で再現させないといけないので、なかなかバグ報告を処理できないでいる。
意外なのは、MIT/GNU Schemeのテスト通過率が良いこと。R7RSの挙動とMIT Schemeの挙動がよく一致していると言える。逆にGambitはread-stringがR7RSのそれとは別物だったり、peek-charはちゃんとあるのにpeek-u8を実現する方法が無い等R7RSにしようとすると苦労する(している)。
というわけで、portの手続きを一通りほぼ実装した。これで残るはreader/writerだけ!もう適当にやっつけて、超おもしろそうなヒープの実装に早く進みたい。。

portの実装用超適当オブジェクトシステム

R7RS Schemeでのportにはおおざっぱに言って、

  • bytevectorと入出力するポート
  • stringと入出力するポート
  • ファイルとテキスト入出力するポート
  • ファイルとバイナリ入出力するポート

の4種類あり、例えばclose-portのようにこれら4種全てをサポートする必要がある手続きもあれば、write-stringのようにテキスト出力ポートだけしかサポートしなくて良い手続きも有る。
こういうものを実装するのに向いているのはオブジェクト指向な言語によくある機構(interfaceとかclassとか)だが、R7RS Smallの標準にはCLOSのようなオブジェクトシステムは当然存在しないため、適当にお茶を濁していく方向となる。
今回実装したランタイムシステムでは、

  • ポートは make-yuniport 手続きに query 手続きを渡して生成する
  • query手続きはポートがサポートする必要のあるメソッドを内包する(いわゆるmessage passingの一種)
  • ポートの状態は全部クロージャに含める。超もったいない。

つまり、ファイルを開くたびに、writeとかreadのためのクロージャが毎回確保される。もったいない。
効率の面の問題は、たとえば、真面目なオブジェクトシステムを実装して解決する方向も考えられるが、ちょっとそこまでする意義があるかどうかは読み切れていない。どうせ非同期I/OとかリストI/Oが無い段階でSchemeのポートI/Oを真面目に使う機会はあんまり無いし、ここはデバッグ用と割り切って単純に実装する方向にした。逆に言うとデバッグ用機能としては重要なので安全確実な動作の方が優先度が高い。
query手続きは先程のポート種別毎に実装される(バッファ/ファイル x テキスト/バイナリ x 入力/出力 = 2x2x2 = 8種類)。

で、これらのqueryを受けとるmake-yuniportが

にある。
make-yuniportは事前にquery手続きを23回起動し、確保したベクタ(simple-struct)に結果をキャッシュする。どうせ使わないものも多いので、ここはon-demandにやっても良いかもしれない。
R7RS Smallにはハッシュテーブルすら無いため、単純にmessage passingを実装するとクエリのたびにO(N)の探索が入ることになってしまう。このため、基本的にはベクタに結果を開き、O(1)で問合せができるように配慮している。
ファイル入力ポートはバッファリングを実装していて、このバッファリングには文字列入力ポートやbytevector入力ポートを使用している。もう一段抽象化して実際のport手続きを使わないで済ませれば良いんだろうけど考えている時間が無い。
今回"テキストファイル用のread手続きの実装"とか"close処理の実装"といったport関連の処理は全てopen手続きの内部に実装されたクロージャということにした。なので、表面的にはportの処理はopenしか実装していないように見える。個人的には、こういう適当な書き方を許容できるのがSchemeの言語コアとしての強さなのかなという気がしている。

ホストSchemeとのバッファのやりとり

今のところSECDV SchemeScheme処理系上に実装されていて、I/Oのためにはバッファをやりとりする必要がある。ここでは、C言語での実装を想定して、bytevectorなバッファでやりとりする、filehandle-read!手続きのように明示的なファイルI/O手続きをいくつか用意してそれだけを使うようにした。

ただ、今のところ、SECDV VMで用意しているホストScheme呼び出し機構はホストSchemeから値を戻せるのは手続きの戻り値のみで、引数がmutateされることを一切想定していないため、そもそもバッファをやりとりするというモデルが成立しない。
... ここはインチキをして特別扱いしている。つまり、

ホストSchemeの呼び出しで特別扱いされているのは他にも有る。例えば、make-vectorは初期値設定なしで使うと unspecified 値が初期値として採用される処理系が多いので、強制的に初期値を設定する手続きに振り替えている。
ホストとのやりとりの問題は、ちょうどプロセス間通信の問題と似ている。APIをプロセス間通信にbindする場合も、結果としてコピーバックする必要のある引数を明示的に設定してやる必要がある。

次の一手

というわけで、最後に残ったのはreaderとwriterとなる。S式readerの骨格部分は既にyuniライブラリの一部として実装してあるので、残るは:

  • S式writer。正規リストだったら(a b c d)のようにスペース区切りで出して、非正規リストは(a b c . d)のようにピリオドを挟む等の配慮ができるやつ。
  • 数値writer。
  • 文字列や特殊なオブジェクトのwriter。ちゃんとescapeする必要がある。
  • 数値reader。
  • 文字列や特殊なオブジェクトのreader。

あたりが残件になっている。Schemeの重要な特徴として read-write invarianceが有り、"writeしたものはreadで元に戻る"を実現する必要がある。同時に、人間に見易い出力を行う display 手続きもあるため、文字列や文字の出力手続きは都合2種類実装する必要がある。
もっとも、真面目なreader/writerを実装するのは結構手間なので、ちゃんとした奴は後回しにして、とりあえずプログラムを実行できるレベルに留める。