Search code examples
common-lisp

Warnings in `cond` testing with `fboundp`, why?


With following Common Lisp minimal example code (from a much more complex code base), I get warnings regarding line ((fboundp (car ',arg)) ,arg) and I can't explain why or how to avoid them.

(defmacro cond-issue (&key (arg '(a b c)))
  (let ((arg-var (gensym "arg-var-")))
    `(let ((,arg-var (cond ((not (listp ',arg)) (list 'arg))
                           ((fboundp (car ',arg)) ,arg)
                           (t ',arg))))
       `(apply #'+ ,,arg-var))))

(cond-issue :arg (e f g))

CCL complains:

;Compiler warnings :
;   In an anonymous lambda form: Undefined function E
;   In an anonymous lambda form: Undeclared free variable F
;   In an anonymous lambda form: Undeclared free variable G

SBCL complains:

; in: cond-issue :arg
;     (E F G)
; 
; caught style-warning:
;   undefined function: common-lisp-user::e
; 
; caught warning:
;   undefined variable: common-lisp-user::f
; 
; caught warning:
;   undefined variable: common-lisp-user::g
; 
; compilation unit finished
;   Undefined function:
;     e
;   Undefined variables:
;     f g
;   caught 2 WARNING conditions
;   caught 1 STYLE-WARNING condition

CLISP does not complain.

With SBCL I tried both: compiling and evaluating the code, with no difference regarding the warnings. [edit: I learned from the answers that I did that wrong]

This would be an (silly) example use case for the second cond form.

(cond-issue :arg (car '(e f g)))
;; ⇒ (apply #'+ e)

What is the explanation, why CCL and SBCL are complaining?
And how can I write the code to satisfy CCL and SBCL?


Solution

  • The purpose of this macro isn't really clear to me, but I think that the question about the nature of these warnings can be addressed. I have added some notes about a possible redesign at the end of the answer.

    The reason that SBCL and CCL are complaining is that they are compiling code. The CLISP repl interprets code. If you open a fresh SBCL repl and put eval in interpreted mode the OP macro can be invoked without triggering warnings:

    CL-USER> (setf *evaluator-mode* :interpret)
    :INTERPRET
    CL-USER> (defmacro cond-issue (&key (arg '(a b c)))
      (let ((arg-var (gensym "arg-var-")))
        `(let ((,arg-var (cond ((not (listp ',arg)) (list 'arg))
                               ((fboundp (car ',arg)) ,arg)
                               (t ',arg))))
           `(apply #'+ ,,arg-var))))
    COND-ISSUE
    CL-USER> (cond-issue :arg (e f g))
    (APPLY #'+ (E F G))
    

    After putting eval back in compiled mode the warnings appear:

    CL-USER> (setf *evaluator-mode* :compile)
    :COMPILE
    CL-USER> (cond-issue :arg (e f g))
    ; in: COND-ISSUE :ARG
    ;     (E F G)
    ; 
    ; caught STYLE-WARNING:
    ;   undefined function: COMMON-LISP-USER::E
    ; 
    ; caught WARNING:
    ;   undefined variable: COMMON-LISP-USER::F
    ; 
    ; caught WARNING:
    ;   undefined variable: COMMON-LISP-USER::G
    ; 
    ; compilation unit finished
    ;   Undefined function:
    ;     E
    ;   Undefined variables:
    ;     F G
    ;   caught 2 WARNING conditions
    ;   caught 1 STYLE-WARNING condition
    (APPLY #'+ (E F G))
    CL-USER> (macroexpand-1 '(cond-issue :arg (e f g)))
    (LET ((#:|arg-var-252|
           (COND ((NOT (LISTP '(E F G))) (LIST 'ARG))
                 ((FBOUNDP (CAR '(E F G))) (E F G)) (T '(E F G)))))
      `(APPLY #'+ ,#:|arg-var-252|))
    

    Looking at the macro expansion above it can be seen that one branch of the code to be compiled contains a function call with an undefined function and two undefined variables: ((FBOUNDP (CAR '(E F G))) (E F G)). The compiler is complaining because it can't compile this code. I think that this is the behavior that you actually want, i.e., you should probably not attempt to circumvent compiler warnings for undefined functions and variables.

    Consider some code that should compile:

    CL-USER> (cond-issue :arg (list 1 2))
    (APPLY #'+ (1 2))
    CL-USER> (macroexpand-1 '(cond-issue :arg (list 1 2)))
    (LET ((#:|arg-var-258|
           (COND ((NOT (LISTP '(LIST 1 2))) (LIST 'ARG))
                 ((FBOUNDP (CAR '(LIST 1 2))) (LIST 1 2)) (T '(LIST 1 2)))))
      `(APPLY #'+ ,#:|arg-var-258|))
    

    Here the form (LIST 1 2) has replaced the previous (E F G), and this presents no problem for the compiler. Note that the resulting form above is (APPLY #'+ (1 2)), and this is not legal lisp code. I think that the intention was to generate (APPLY #'+ '(1 2)) instead. Further, the invocation (cond-issue :arg 42) yields (APPLY #'+ (ARG)) where I suspect that (APPLY #'+ '(42)) was desired. Here is a slightly modified version of cond-issue that fixes these problems:

    (defmacro cond-issue-fixed (&key (arg '(a b c)))
      (let ((arg-var (gensym "arg-var-")))
        `(let ((,arg-var (cond ((not (listp ',arg)) (list ',arg))
                               ((fboundp (car ',arg)) ,arg)
                               (t ',arg))))
           `(apply #'+ ',,arg-var))))
    

    Again, I think that you should not try to circumvent these compiler warnings, but as an experiment, and in the spirit of patching together a half-baked solution, you could wrap cond-issue in a macro that checks the arguments first:

    (defmacro cond-issue-check (&key (arg '(a b c)))
      `(when (every (lambda (item)
                      (or (boundp item)
                          (fboundp item)))
                    (remove-if-not #'symbolp ',arg))
         (cond-issue-fixed :arg ,arg)))
    

    This solution has a couple of caveats: boundp and fboundp check for bindings in the global environment, but they cannot inspect local bindings (and I'm not sure that there is any simple way to do this). Also, the wrapper will reject macro invocations like (cond-issue-check :arg 42) with an error. The cond-issue-check wrapper could be augmented to better validate input, but the current definition should suffice to make the point. Now would be a good time to interrogate the purpose of the macro, whether it needs to be a macro at all, and to revisit the design.

    CL-USER> (cond-issue-check :arg (e f g))
    NIL
    CL-USER> (cond-issue-check :arg (list 1 2))
    (APPLY #'+ '(1 2))
    CL-USER> (let ((a 1) (b 2))
               (cond-issue-check :arg (list a b)))
    NIL
    

    In the final example above the checking wrapper can't determine whether the local bindings for a and b are bound. The original, unwrapped cond-issue could be called when local bindings are needed, but this means that warnings may be triggered for unbound functions or variables.

    In any case, the above fix will not stop all warnings for missing definitions. In general I wouldn't recommend trying to suppress these types of warnings, and I wouldn't recommend using cond-issue-check. It is probably best to let the compiler do its job.

    Redesigning the Macro

    It seems that the OP goal is to take as input forms which describe other forms to which #'+ can be applied, and from this to create forms which are not required to be valid in their current environment. If this is the case, here is an alternative design.

    (defmacro cond-something ((&rest args))
      `(if (and (symbolp ',(car `,args))
                (fboundp ',(car `,args)))
           `(apply #'+ ',,args)
           `(apply #'+ ',',args)))
    

    Here if the first element of an input form is a symbol which is fbound, a form which applies #'+ to the result of calling the input form is generated. Otherwise a form which applies #'+ to all of the arguments is generated.

    Some example invocations:

    CL-USER> (cond-something (42))
    (APPLY #'+ '(42))
    CL-USER> (eval *)
    42
    CL-USER> (cond-something (list 1 2 3))
    (APPLY #'+ '(1 2 3))
    CL-USER> (eval *)
    6
    CL-USER> (cond-something (mapcar (lambda (x) (* 2 x)) '(1 2 3 4 5)))
    (APPLY #'+ '(2 4 6 8 10))
    CL-USER> (eval *)
    30
    CL-USER> (cond-something (e f g))
    (APPLY #'+ '(E F G))
    CL-USER> (eval *)
    ; Evaluation aborted on #<TYPE-ERROR expected-type: NUMBER datum: E>.
    

    As previously mentioned, fboundp inspects global bindings, but it cannot tell whether lexical variables are bound to functions, i.e., those created with flet or labels. Here are examples showing the use of a global function for which cond-something generates the expected output, and the use of a local function for which cond-something would not generate the code one might naively expect:

    CL-USER> (defun g (x y) (list (* x y)))
    G
    CL-USER> (cond-something (g 2 3))
    (APPLY #'+ '(6))
    CL-USER> (flet ((f (x y) (list (* x y))))
               (cond-something (f 2 3)))
    (APPLY #'+ '(F 2 3))