#:g1: Common Lispでアリティの不整合をコンパイルエラーにする

Posted 2019-08-11 20:29:48 GMT

思えば、Common Lispでアリティ(引数の個数)の不整合でエラーを喰らうのは実行時が多い気がしますが、ANSI Common Lisp規格ではコンパイル時に弾くことってできないんでしょうか。

……と思ってHyperSpecにあたってみましたが、コンパイル時にアリティの不整合を検知した場合には、エラーにしてしまうコンパイラがあっても良さそうではありました。

Common Lispは動的言語なので、実行時でも関数等の再定義が可能ですが、上記の3.2.2.3 Semantic Constraintsを眺める限りでは、どちらかといえば、コンパイラ動作の方に重きを置いているように見え、再定義されなくても良い場合を幾つか挙げています。

逆に再定義が保証されることを主として考えるとすると、少なくともnotinline宣言がついてない場合の再定義は、関数が実行時に確実に置き換わることは期待できなさそうです。

また、コンパイラ/インタプリタ動作の整合性とは別に、アリティのチェックについても記載がありますが、エラーは、実行時もしくはコンパイル時に挙げるとあります。

上記は、関数呼び出し時のエラーについての規定なので、基本的に実行時エラーだけで良さそうにも思えますが、マクロ展開等も考慮するとコンパイル時も含めないといけないのでしょうか。
ともあれ、safe callの観点からもコンパイル時に検出することも可能ではありそうです。

ちなみに、組み込み関数はsafe callの観点からすると、アリティの不整合はコンパイル時にエラーにできそうです。

処理系の実際を確認する

呼出しのアリティに不整合があった場合にコンパイルエラーになっても良さそうではあるのですが、実際の処理系の挙動を確認してみました。

このようなファイルをコンパイルして、

(defun foo (x y)
  (list x y))

(defun bar (x y) (foo x))

下記のように実行してみます。

(bar 1 2)
>>> invalid number of arguments: 1

処理系のメジャーなところは一通り確認してみましたが、コンパイルエラーにする処理系はECLのみのようで、他は警告は出すもののコンパイルは通し、faslをloadして実行したら当然実行時エラーです。
まあ大体Common Lispってこんな感じの動作でしたね、というところ。
むしろECLの動作のほうが意外かもしれません。

処理系 compile-file
SBCL 警告あり
CLISP 警告あり
LispWorks 警告あり
Allegro CL 警告あり
Clozure CL 警告あり
ECL コンパイルエラー
MkCL 警告なし
Clasp 警告なし
CMUCL 警告あり

アリティの不整合をコンパイルエラーにしたい

さて、規格上はコンパイル時にアリティの不整合を検出してエラーにしても良さそうだけれど、実際のところデフォルトでそういう挙動をする処理系は、殆ど存在していないことが分かりました。

コンパイル時でもなんでもできるCommon Lispなので、工夫はあれこれできそうですが、とりあえずコンパイラマクロを試してみましょう。
とりあえずは、何もしないコンパイラマクロを定義するのみですが、展開時に引数チェックでエラーになることが期待できます。

(defun foo (x y)
  (list x y))

(define-compiler-macro foo (x y) `(foo ,x ,y))

(defun bar (x y) (foo x))

処理系 compile-file
SBCL 警告あり
CLISP コンパイルエラー
LispWorks コンパイルエラー
Allegro CL 警告あり
Clozure CL コンパイルエラー
ECL コンパイルエラー
MkCL 警告なし
Clasp コンパイルエラー
CMUCL 警告あり

軒並エラーになると予想していましたが、コンパイルが通ってしまうものもある様子。
これは、積極的にエラーにする他ないのかもしれません。
ということで下記のように書いてみました。

(eval-when (:compile-toplevel :load-toplevel :execute)
  (define-condition argument-mismatch (program-error)
    ((form :initarg :form :reader argument-mismatch-form)
     (text :initarg :text :reader argument-mismatch-text))
    (:report (lambda (c s)
               (format s
                       "~A:~%~S~%"
                       (argument-mismatch-text c)
                       (argument-mismatch-form c))))))

(defun foo (x y) (list x y))

(define-compiler-macro foo (&whole w &rest args) (etypecase (length args) ((eql 2) w) ((integer * 1) (error 'argument-mismatch :text "Too Few Arguments" :form w)) ((integer 3 *) (error 'argument-mismatch :text "Too Many Arguments" :form w))))

(defun bar (x y) (foo x))

しかし、SBCL、Allegro CLはコンパイルを通す様子。

処理系 compile-file
SBCL 警告あり
CLISP コンパイルエラー
LispWorks コンパイルエラー
Allegro CL 警告あり
Clozure CL コンパイルエラー
ECL コンパイルエラー
MkCL 警告なし
Clasp コンパイルエラー
CMUCL 警告あり

SBCLでは、トラップしたいなら、*break-on-signals*を設定しろと警告が出るので、SBCL、Allegro CL、CMUCLはそういうポリシーなのでしょう。

; caught WARNING:
;   Error during compiler-macroexpansion of (FOO X). Use *BREAK-ON-SIGNALS* to
;   intercept.
;   
;    Too Few Arguments:
;   (FOO X)

まとめ

Common Lispでアリティの不整合をコンパイルエラーにする方法を探ってみましたが、

の組み合わせで大抵の処理系では実現できそうです。

ちなみに、型の不整合もチェックしたいところですが、SBCLであれば、今回のコンパイラマクロの手法を応用して、deftransformあたりを使えばできなくもなさそう。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus