現在ゆっくり製作中
現在のところ、eval/sv内にdynamic-windが設置された場合の挙動に、仕様レベルでの問題がある事が判明しています。
詳細については「問題点」の項目を確認してください。
概要
evalによる式の評価を、監視付きで実行します
必要要件
- Gauche-0.8.13
- これより古くても新しくても、動かない可能性が高いです
ライセンス
修正BSD風ライセンスです。
tarball同梱のCOPYINGに詳細(といっても、定型文のみですが)が書いてあります。
ダウンロード
パッケージ
- 不完全お試し版(マクロ展開の監視無し、一部のsyntaxの監視無し、一部の手続きの無限ループ監視無し、r5rs部分のみの実装)
リポジトリ
ChangeLog
インストール
gauche-package installに対応してます。
gauche-package install eval-sv-x.y.z.tgz
リポジトリから持ってきた場合は、インストール可能な権限で以下を実行。
./DIST gen
./configure
make all check
make install
が、別にインストールしなくても使えます。
その場合は、適当に、libディレクトリをモジュールロードpathに追加して下さい。
gauche.nightプレゼン
デモアプリ
S式バトラー
S式バトラーのソース(物凄い汚い注意)
理論と応用
使い方
eval-svモジュール
このモジュールは、ステップ監視付きの評価器(のジェネレータ)を提供する。
サンプルコード
(use eval-sv)
(define-values (eval/sv env)
(make-eval/sv :isolate-port? #f))
;; eval/sv手続きと無名モジュールが返る
(define (sv type symbol expr args return except)
(print `(apply ,expr ,args))
;; ここで必要に応じて処理を行うが、
;; 重要なのは以下を実行する事
(apply expr args))
;; 実行してみる
(eval/sv '(+ 1 3) sv)
;; => printしてから4が返る
;; 別の監視手続きで実行してみる
(define (sv2 type symbol expr args return except)
(except "raise error"))
(eval/sv '(+ 1 3) sv2)
;; => error例外
(import/sv env 'eval/sv eval/sv)
;; import/svで、外部の手続きを
;; (比較的)安全に持ち込める
;; ここではeval/sv自身を内部に持ち込んでみた
;; (勿論、ちゃんと動くように作ってます)
(eval/sv `(eval/sv '(+ 1 3) ,sv2) sv)
;; => 二回printしてからerror例外
;; (監視手続きがmergeされた)
詳細
- make-eval/sv手続きは、eval/sv手続きを生成する。
- make-eval/sv手続きによって生成された複数のeval/sv手続きは、それぞれ独自に無名モジュール空間を内包するので、それぞれの名前空間が衝突するような事はない(ので、安心してdefine等ができる)。
(use eval-sv)
;; eval/svを生成する
(define-values (eval/sv env)
(make-eval/sv :default-sv-proc #f
;; ↑デフォルトの監視手続き。デフォルト値は#f。
;; ↑ここで設定せずに、eval/sv実行時に指定してもよい
;; ↑(その方が分かりやすい)
:isolate-port? bool
;; ↑current-*-portをダミーに差し替えるフラグ。
;; デフォルト値は#t。
;; (current-*-portがブロックデバイスだった場合、
;; ブロッキング停止が発生する可能性がある為、
;; 安全性を確保する為に、#tにしておいた方が良い。
;; 自前でcurrent-*-portでブロッキング停止が
;; 発生しない保証をしているなら、#fにしてもよい。)
:parent-module 'module-name
;; ↑make-eval/svがextendする、
;; 無名モジュールの親モジュール名。
;; シンボルで指定する事(無名モジュール不可)。
;; 親モジュールは、規定の方法で初期化されている事。
;; eval-sv.templateによって、ある程度の種類の
;; 目的別テンプレートモジュールを用意してある。
;; それらを使う場合は
;; :parent-module [atmn 'r5rs+]
;; のような指定をすればよい。
:bind-alist `((symbol ,expr) ...)
;; ↑追加束縛のalist。
;; 自動的に(import/sv env symbol expr)される。
))
;; 監視手続きを定義する
(define (sv-proc type symbol expr args return except)
... ; 値を監視し、必要な処理を行う事
... ; 重要なのは、以下のapplyを実行し、
... ; その返り値を返す必要があるという点
(apply expr args))
;; 実際に式を評価する
(eval/sv expr sv-proc)
;; eval/svの環境内に、新しい束縛を後付け追加する
(import/sv env 'undefined undefined)
;; 内部で無限ループする可能性のある手続き等は、
;; 自前で何らかの対策を行い、
;; 無限ループしない事を保証する必要がある
(use util.list)
(import/sv env 'intersperse (lambda (item source-list)
(if (circular-list? source-list)
(error "this is circular-list")
(intersperse item source-list))))
(eval/sv '(undefined) sv-proc) ; => #<undef>
(eval/sv '(intersperse '+ '(1 2 3)) sv-proc) ; => (1 + 2 + 3)
eval-cu-liteモジュール
このモジュールは、実行カウント付き評価器(のジェネレータ)を提供する。
- (make-eval/cu ...)によって、eval/cu及び、eval/cuが内部に保持している環境が得られる。
- (eval/cu expr threshold)で、exprを評価する。thresholdには正の数値を指定する。thresholdステップ内にexprが終了しない場合は、規定の例外が投げられる。
- (これとは別に、マクロ展開や、do等のループが一定回数に達した場合も、規定の例外が投げられる。)
- 尚、例外と共に続行する為の継続も投げられるので、その継続を実行すれば評価を再開する事も一応可能。
- (eval-cu-last-count)で、最後に正常終了した際の、最後の実行値が得られる。
- (例外終了した時は(eval-cu-last-count)からは正しい実行値は得られない。その時は、例外オブジェクトから正しい実行値を得る事。)
- あとは大体、eval/svと同じ。eval/cu自身をimport/svする事も可能。
(use eval-cu-lite)
(define-values (eval/cu env)
(make-eval/cu :isolate-port? bool ; eval/svと同じ
:parent-module [atmn 'r5rs+] ; eval/svと同じ
:bind-alist '() ; eval/svと同じ
:default-threshold 200
;; この回数、手続きまたは
;; special formが実行されたら例外終了
;; (この値はeval/cuの第二引数省略時に
;; fallback値として使われる)
:macro-threshold 200
;; この回数マクロ展開されたら例外終了
:loop-threshold 200
;; この回数ループ実行されたら例外終了
))
(guard (e ((<eval-cu-exceeded> e)
(let ((type (ece->type e)) ; 'proc or 'macro or 'loop
(count (ece->count e)) ; その時点でのカウンタ値
(continue (ece->continue e))) ; 続行する為の継続
;; ※continueを使って続行した場合も、カウンタ値は
;; 続行され続ける事に注意
;; (次回に例外が呼び出されるのはthresholdの次の倍数)
;; 実際のオーバーカウント処理をここに書く
...))
(else
;; その他のエラー処理をここに書く
...))
;; exprにはevalしたい式を、thresholdにはprocカウントの閾値を設定する
;; (macroカウントとloopカウントの閾値はmake-eval/cu時以外は変更不可)
(eval/cu expr threshold))
(eval-cu-last-count)
;; これを実行する事で、最後に実行したeval/cuのカウント値が得られる。
;; (これは、eval/cuは正常に終了したが、
;; カウントがどこまで進んだのか知りたい時に使える)
;; これはparameterなので、入れ子やthread動作等で不安がある場合は、
;; parameterizeしておけば安全に参照する事が出来る。
eval-sv.templateモジュール
faq
- "restricted to access to port"というエラーが出た。
- デフォルトでは、安全の為、eval/sv実行時には、current-input-portとcurrent-output-portは、アクセスするとこのエラー例外を投げるvportに差し換えられます。
- これは、current-input-portやcurrent-output-portがブロッキングする可能性がある為です。
- 別にブロッキングしても構わない場合や、自前でcurrent-input-portを差し替える等して「決してブロッキングしない」事が保証されている場合は、make-eval/sv時にキーワード「:isolate-port? #f」を与える事で、current-input-portとcurrent-output-portをそのままにする事ができます。
- 尚、current-error-portは常にそのままです。一応current-error-portもブロッキングする可能性はあるのですが、このportを無効化すると、エラー例外の再帰起動が起きてGaucheプロセス自体が死んだり、色々と不便すぎる状態になってしまったので、常にそのままにする事にしました。
問題点
- eval内でdynamic-windを使われた内部で、returnやexceptによる脱出を行うと、dynamic-windで設定されたbefore thunkやafter thunkが実行されてしまう
- 現段階ではとりあえず、これは「仕様」という事にしておきます。
- 結構奥深い問題なので、余裕のある時に考え直します。
バグレポート
あとで。
内部情報
各値の扱いについて
- eval/svの外から、eval/svの中へと、何らかの値を持ち込む際には、通常、import/svを使う事になっている。
- import/sv自体は、束縛対象の値をenfold-entityに通してから、モジュールに束縛しているだけ。実際に怪しい処理を行っているのはenfold-entity。
- import/svやenfold-entityを通さずにeval/sv内に持ち込んだ値には、監視が付かない。
- enfold-entityに通した値は、値の種別によって異なる処理がなされる。具体的にどのような処理がなされるかは以下に記す。
手続き及びmethod
- 監視手続きに手続きの実体と引数を渡す手続きが生成され、それが返される。
- 要は、(eval/sv expr sv-proc)のsv-procに元の手続きが渡されるような手続きに、置き換えられる。
マクロ
- enfold-entity自体は、マクロに対して特別な処理は行わない(今のところは)。同じ内容をそのまま返す。
- 但し、マクロは展開後に素のシンボルが出現するもの(伝統的なマクロ?)、モジュール指定付きのidentifierが出現するもの(健全なマクロ?)の二種類があり、それぞれに問題がある。
- 素のシンボルが出現するマクロでは、出現したシンボルに対する束縛が該当モジュール内に存在する必要があるが、import/svやenfold-entityでは、そこまでの面倒は見れない。つまり、マクロ展開後に出現するシンボルに対する束縛は、ユーザ自身が予め用意する必要がある。
- モジュール指定付きのidentifierが出現するマクロでは、前述の問題こそ発生しないが、展開結果が別モジュール内の手続きになった場合に、その手続きに対して監視手続きを含めるようにするのが難しい。
- 一部のプリミティブな機能を提供するマクロは、後述のspecial formとほぼ同じ扱いになる。
special form
- enfold-entityにかけると、マクロに置換される。
- ほとんどのspecial formは、マクロの特殊な形として実装でき、また、数も限られているので、事前に置換テーブルのようなものを用意しておき、enfold-entityの対象がspecial formであるなら、そのテーブルを引いて実体を得るようにする。
それ以外の値
- それ以外の数値、文字列、list、hash-table、その他のインスタンス類は全て、そのまま。
- listやhash-table内に手続きが入っていた場合は、監視は付かずにそのままとする(副作用無しに差し替える事が不可能な為)。
モジュール構造と関係の説明
- eval-sv.scm
本体。以下のモジュールを参照している。
- cf-arity.scm
手続きのarityを偽装するモジュール。単体使用可能。
- nullport.scm
current-*-portを無効なportに差し換えるモジュール。単体使用可能。
- common.scm
他のモジュールから参照されている、gauche.h的なモジュール。
- genroot.scm
eval-svのテンプレートモジュールのrootを生成し、ついでに各種テーブルも定義束縛しているモジュール。これも他の多くのモジュールから参照されている。
- gensyntax.scm
R5RS及びGaucheの提供しているspecial form及びmacroを再定義しているモジュール。
- genproc.scm
R5RS及びGaucheの提供している手続きを再定義しているモジュール。
- enfold.scm
外部から手続き等をimportする時の為に、監視手続きを追加する為のモジュール。
- template.scm
'gaucheモジュールや'schemeモジュールのように、前述の再定義された束縛をまとめたテンプレートモジュールを生成するモジュール。
- form.scm
実際のeval/sv時の、special formやマクロの展開を担当する。
- eval-cu-lite.scm
eval-svをラッピングして、単純な用途には簡単に使えるようにしたもの。
作った感想
一番最初に想像していた仕様
( http://d.hatena.ne.jp/ranekov/20080127/1201369856 )
- 当初に想定していたよりもずっとヘビーな作業になった。
- マクロとspecial formの扱いを忘れていた/軽く見すぎていた為。
- やっぱSICPは真面目にやっとくべきだった。
- しかし、停止問題回避には、評価プロセス隔離の仕組みだけで充分だった事に、もっと早く気付いていれば……。
- とは言え、叩き台として、こういう機能が充分に役立つ事は示された気がする。自分の中で。
- ただ、もしこれを真面目に利用するには、今含まれてる黒魔術的部分をもう少し追い出す必要がある気はする。
- インターフェースが微妙。
- make-eval/svの返り値は、eval/svとenvに分かれてるが、適当にクラス化して、スロット値としてenvとかを持つようにした方が良かった。そしてobject-applyでeval/svを実行。
- これは元々、使われるのはeval/svだけで、envはほぼ使われないだろうという事で、envを使わない時は、(define eval/sv (make-eval/sv ...))と書けるようにしようという風に考えていたものの、結構import/svするようなので、実際にはほぼdefine-valuesを使うのが定型になってしまった。
- 具体的には、env内にeval/sv自身を持ち込む際は、import/sv経由にせざるをえない(その時点ではeval/svは生成されてないので、:bind-alistでは無理)ので、それが原因でenvが必要になる事が多かった。
- :isolate-port?のデフォルトが微妙。
- writeやdisplayは手軽な出力器なので、簡単な確認であれば気軽に使いたいが、writeやdisplayを使うには:isolate-portに#fを指定しておかなくてはならない。
- そもそもの問題は、デフォルトの(current-input-port)が通常はstdinである事にある。
- デフォルトの(current-input-port)、つまりstdinからreadしようとすると、簡単にブロックしてしまい、eval/svの目標である「停止しない」があっさりと破綻してしまう。
- これを嫌って、:isolate-portというキーワードにより、デフォルトの(current-*-port)を無効portに差し換える仕様とした。
- しかし、これはkiss原則に反しているし、Paul GrahamのArc的な思想にも反している。「安全の為に効率を犠牲にしている」とも言える。
- Paul Grahamが「(他の何かを犠牲にして)ユーザが自分の足を撃つのを防ごうとすべきではない」的なニュアンスの事を言っていた。自分もそう思う。
- 故に、:isolate-port?を用意する事自体は悪くないが、デフォルト値を#tにしたのは失敗だったと思った。
- 「eval」にしたが、with-signal-handlerのように、thunkを受け取って、その中でのみ監視付き実行をするようにした方が、より便利だった気がする(可能かどうかは謎。おそらくrun_loopに手を入れる必要がある)。
開発メモ
ここには、開発中の一時メモを書きます。
開発者以外が読む価値はありません多分。
残り作業
- gensyntax.scm 早く処置が必要なsyntaxを実装する事
- genproc.scm 早く処置が必要なprocを実装する事
- template.scm もう少し整頓する事(r5rs+系の提供)
- eval-sv.scm (template.scm完成後、デフォルトテンプレートの変更が必要)
- 実行プロセス隔離モジュールの作成
r5rs+で提供すべき手続き候補
思い付いたらどんどん書く事。厳密なチェックは最後にやる。
- srfi-1全部
- debug-print
- format
- flush
- object-applyやmake等のmethod
- class-ofとかクラス関連
- gauche.parameter
- キーワード関係
- hash-table関係
- 例外関係
チェックリスト
- test caseに追加すべきテスト内容
- 手続きとspecial formの、循環リスト対策
- 手続きとspecial formの、無限ループ対策
- 一部のマクロ展開後に出現する内部手続きの、監視追加、循環リスト対策、無限ループ対策
- 他には???
その他のメモ
- evalするとdefine-macro式になるS式、を生成するのに、現在は多重quasiquoteを使っているが、これが非常に分かりにくい。template treeを作って、その特定の枝を置換するような方式にした方が良いので、そういう手続き等を用意する事
最終更新 : 2008/04/13 03:36:46 JST