(rontolisp) docs

コンパイルされた eval の制限

eval は 3 つのバックエンドすべてで動作します。インタプリタでは完全なツリーウォーク評価器そのものです。WASM コンパイラと JVM コンパイラは、フォームを実行時に実行する小さなツリーウォークインタプリタ(_eval/_apply/_store とヘルパー _envLookup/_lookup)を出力に埋め込みます。そのため、別個の評価器やパーサーは不要です。

コンパイルされた eval(WASM および JVM)は、レキシカル環境と永続的なグローバル環境を実装し、インタプリタとの一致を目指しています。自己評価アトム、変数参照、クロージャ、特殊フォームと高階関数(letlambdacondwhiledotimessetqsetfpushpopfuncallmapcarmapcreduce、ネストした eval など)、および任意の関数や解釈されたクロージャの適用はすべて、インタプリタと同じように動作します。すべてを列挙するのではなく、相違点を以下に挙げます。

コンパイルされた eval の制限

コンパイルされた eval(WASM および JVM)がインタプリタと異なるのは、以下の場合だけです。

  • let の束縛リストは ((name value) ...) の形式を使う必要があります(裸の (let (x) ...) はサポートされません)。
  • 比較演算子は eval の中では二項演算です。 コンパイルされたトップレベルコードは可変長の =<><=>= と可変長の min/max/gcd/lcm をサポートします(コンパイル時にネストした二項演算に脱糖されます)。しかしその脱糖は eval によって実行時に解釈されるフォームには及ばず、そこではこれらの演算子は引数を 2 つ取り、それ以上の引数は無視されます(そのため (eval '(= 1 1 2))(= 1 1) を評価して真を返します)。+ - * / list はどこでも完全に可変長です。8 個以上のパラメータを持つユーザー関数は nil を返します。
  • 失敗するエッジケース。 引数なしの (+)/(-)/(*)/(/) は実行時に失敗します(WASM ではトラップ、JVM では例外)。単項の (- x)(/ x) はインタプリタと同様に符号反転/逆数化を行います。
  • 実行時に構築された lambda はラムダリストキーワードを解釈しません。 コンパイルされた defun/lambda フォームは &optional/&rest/&key をサポートし(コンパイル時に脱糖されます)、そのようなコンパイル済み可変長関数を eval から呼び出すことはできます。しかし eval されるフォームの中にのみ存在する lambda、例えば (eval '(funcall (lambda (&rest r) r) 1 2)) はパラメータを位置的に束縛します — &rest は通常のパラメータ名として扱われます。
  • 多倍長整数への昇格はありません。 実行時 eval インタプリタ内の算術演算は固定幅整数を使い、オーバーフロー時にラップします。コンパイルされたコード自体が多倍長整数に昇格する JVM 上でも同様です。
  • 未束縛の変数はシンボル自身に評価されます。 インタプリタは The variable x is unbound を通知しますが、実行時 eval にはエラー通知の手段がなく、代わりにシンボルを返します。呼び出し位置にある未定義の関数は nil を返します。
  • トップレベルのグローバル変数はコンパイルされたコードからライトスルーで共有されます。 トップレベルの setq/defvar/defparameter/defconstant は、その値を実行時 eval のグローバル環境にミラーします。そのため、eval された式は、コンパイルされたプログラムが定義したグローバルを読むことができます(例: (setq add10 (make-adder 10)) の後に (eval '(funcall add10 100))110 を返します)。このミラーは一方向です。後で eval がそのような変数を再代入しても、コンパイルされたコードは自身のコピーを読み続けます。
  • let*dodo*dolistreturndefvardefparameterdefconstantincfdecfformaterrorecaseetypecaseccaseconcatenatewith-open-file およびファイルストリーム関数(openclosewrite-line)はサポートされません。 これらのフォームはコンパイル時にのみ展開・処理されます。実行時 eval インタプリタはこれらを認識しません。シーケンス関数(lengthreversemembermember-iffindfind-ifpositioncountassocassoc-ifgetflastbutlastremoveremove-ifremove-if-notremove-duplicatesdeletedelete-ifdelete-if-notsubstitutensubstitutenconccopy-listnreversemake-listunionintersectionset-differenceadjoinidentitymapcansorteverysome)と princ-to-string/prin1-to-string は、コンパイルされた関数レジストリを通じて解決されるため動作します。ただしシーケンス関数・連想リスト関数の :test/:key キーワードはコンパイル時の展開であるため、実行時 eval フォームの中では黙って無視され、比較は eql のままになります(eval 内でもこれらを適用するのはインタプリタバックエンドのみです)。
  • defmacro とバッククォートはコンパイル時のみです。 コンパイルパスでは、ユーザーマクロはコンパイラの実行前に完全展開され(定義も取り除かれ)、バッククォートのテンプレートはリーダーで展開されます。コンパイル済みプログラムの実行時 eval/readdefmacro もバッククォート文字も認識しません。 macroexpand/macroexpand-1 も同様です: リテラルのクォートされた引数を持つ呼び出しはコンパイル時に展開結果へ畳み込まれ、実行時 eval はこれらの関数を認識しません(対照的に gensym はファーストクラスのラッパーを持つため動作します)。
  • defstruct はコンパイル時のみです。 トップレベルの defstruct はコンパイル前に生成関数へ展開されるため、コンストラクタ/アクセサ/述語を eval から呼び出すことはできますが、eval されるフォームの中で新しい構造体を定義したり、アクセサを setf の place として使うことはできません。
  • CLOS サブセットはコンパイル時のみです。 defstruct と同様に、トップレベルの defclass/defgeneric/defmethod はコンパイル前に展開されます。総称関数・reader/accessor・コンストラクタを eval から呼び出すことはできますが、eval されるフォームの中でクラスやメソッドを定義することはできず、make-instance/slot-valueeval の中では認識されません(コンパイル時のクラスレジストリを通じて解決されるためです)。
  • rontolisp パッケージの関数はサポートされません。 rontolisp:versionrontolisp:list-functionsrontolisp:list-macrosrontolisp:list-special-formsrontolisp:fetchrontolisp:http-handlerrontolisp:awaitrontolisp:thenrontolisp:promiseprontolisp:json-parserontolisp:json-stringify は直接コンパイルされます(定数、インライン呼び出し、または組み込まれるライブラリ関数)。実行時 eval/load はこれらを認識しません。

これらの相違は設計に由来します。実行時 eval は、実際に出力にコンパイルされた関数のコンパイル時レジストリに対して名前で演算子を解決し、組み込み関数はコンパイルされたコードと共有されます。