コンパイルされた eval の制限
eval は 3 つのバックエンドすべてで動作します。インタプリタでは完全なツリーウォーク評価器そのものです。WASM コンパイラと JVM コンパイラは、フォームを実行時に実行する小さなツリーウォークインタプリタ(_eval/_apply/_store とヘルパー _envLookup/_lookup)を出力に埋め込みます。そのため、別個の評価器やパーサーは不要です。
コンパイルされた eval(WASM および JVM)は、レキシカル環境と永続的なグローバル環境を実装し、インタプリタとの一致を目指しています。自己評価アトム、変数参照、クロージャ、特殊フォームと高階関数(let、lambda、cond、while、dotimes、setq、setf、push、pop、funcall、apply、mapcar、mapc、reduce、ネストした eval など)、および任意の関数や解釈されたクロージャの適用はすべて、インタプリタと同じように動作します。すべてを列挙するのではなく、相違点を以下に挙げます。
コンパイルされた eval の制限
コンパイルされた eval(WASM および JVM)がインタプリタと異なるのは、以下の場合だけです。
letの束縛リストは((name value) ...)の形式を使う必要があります(裸の(let (x) ...)はサポートされません)。- 実行時に構築された
lambdaはラムダリストキーワードを解釈しません。 コンパイルされたdefun/lambdaフォームは&optional/&rest/&keyをサポートし(コンパイル時に脱糖されます)、そのようなコンパイル済み可変長関数をevalから呼び出すことはできます。しかし eval されるフォームの中にのみ存在する lambda、例えば(eval '(funcall (lambda (&rest r) r) 1 2))はパラメータを位置的に束縛し —&restは通常のパラメータ名として扱われます — 引数の個数も検査しません。ラムダリストキーワードを持たない実行時 lambda は、インタプリタと同様に引数の個数の誤りをprogram-errorとして通知します。 - 未束縛の変数はシンボル自身に評価されます。 インタプリタは
The variable x is unboundを通知しますが、実行時evalにはエラー通知の手段がなく、代わりにシンボルを返します。呼び出し位置にある未定義の関数はnilを返します。 - グローバル変数はコンパイルされたコードと
evalで共有されます。setq/defvar/defparameter/defconstantは、その値を実行時evalのグローバル環境にミラーします。そのため、eval された式は、コンパイルされたプログラムが定義したグローバルを読むことができます(例:(setq add10 (make-adder 10))の後に(eval '(funcall add10 100))は110を返します)。eval された代入(setq、setf、push、pop)はコンパイルされた変数にも書き込むため、コンパイルされたコードからも見えます。スペシャル変数(defvar/defparameter)は、コンパイルされたコードと同じく現在の動的束縛を通して読み書きされます。その変数を束縛するletの内側では、evalはその束縛を読み、その束縛に代入します。(set ...)(または(setf (symbol-value ...) ...))も両方に書き込みます。ミラーされるのはグローバル変数だけです。トップレベルフォームのレキシカル変数 —let/loop/doの変数 — はミラーされません。これはevalが空のレキシカル環境で評価するという Common Lisp の規定に一致します((let ((x 5)) (eval 'x))はどのバックエンドでもグローバルのxを、それが無ければシンボル自身を読みます)。 let*、do、do*、dolist、return、defvar、defparameter、defconstant、incf、decf、format、error、ecase、etypecase、ccase、concatenate、with-open-fileおよびファイルストリーム関数(open、close、write-line)はサポートされません。 これらのフォームはコンパイル時にのみ展開・処理されます。実行時evalインタプリタはこれらを認識しません。シーケンス関数(length、reverse、member、member-if、find、find-if、position、count、assoc、assoc-if、getf、last、butlast、remove、remove-if、remove-if-not、remove-duplicates、delete、delete-if、delete-if-not、substitute、nsubstitute、nconc、copy-list、nreverse、make-list、union、intersection、set-difference、adjoin、identity、mapcan、sort、every、some)とprinc-to-string/prin1-to-stringは、コンパイルされた関数レジストリを通じて解決されるため動作します。defmacroとバッククォートはコンパイル時のみです。 コンパイルパスでは、ユーザーマクロはコンパイラの実行前に完全展開され(定義も取り除かれ)、バッククォートのテンプレートはリーダーで展開されます。コンパイル済みプログラムの実行時eval/readはdefmacroもバッククォート文字も認識しません。macroexpand/macroexpand-1も同様です: リテラルのクォートされた引数を持つ呼び出しはコンパイル時に展開結果へ畳み込まれ、実行時evalはこれらの関数を認識しません(対照的にgensymはファーストクラスのラッパーを持つため動作します)。defstructはコンパイル時のみです。 トップレベルのdefstructはコンパイル前に生成関数へ展開されるため、コンストラクタ/アクセサ/述語をevalから呼び出すことはできますが、eval されるフォームの中で新しい構造体を定義したり、アクセサをsetfの place として使うことはできません。#S(...)リテラルも同様にコンパイル前に解決されるため、evalの中では認識されません。- CLOS サブセットはコンパイル時のみです。
defstructと同様に、トップレベルのdefclass/defgeneric/defmethodはコンパイル前に展開されます。総称関数・reader/accessor・コンストラクタをevalから呼び出すことはできますが、eval されるフォームの中でクラスやメソッドを定義することはできず、make-instance/slot-valueはevalの中では認識されません(コンパイル時のクラスレジストリを通じて解決されるためです)。 rontolispパッケージの関数はサポートされません。rontolisp:version、rontolisp:fetch、rontolisp:http-handler、rontolisp:await、rontolisp:futurep、rontolisp:json-parse、rontolisp:json-stringifyは直接コンパイルされます(定数、インライン呼び出し、または組み込まれるライブラリ関数)。実行時eval/loadはこれらを認識しません。
これらの相違は設計に由来します。実行時 eval は、実際に出力にコンパイルされた関数のコンパイル時レジストリに対して名前で演算子を解決し、組み込み関数はコンパイルされたコードと共有されます。