lispmetaprogrammingelisp

ELisp macro different expansions based on static parameters


I’m trying to understand if it’s possible/good practice to write macros that expand differently depending on whether the arguments are constants or symbols. As an example (in Emacs 30.0.50):

(defmacro list-length (x)
  (cond (`(symbolp ,x) `(eval (length ,x)))
        (t (length x))))

The different ways I’d expect it to be expanded are as follows:

1.

(let ((x (list 1 2 3 4 5)))
  (macroexpand '(list-length x)))

=> (eval (length x))

OR

2.

(macroexpand '(list-length (1 2 3 4)))

=> 4

A more real-world use case for myself is in something like https://github.com/nealsid/simpleproj/blob/main/util.el#L18, and what I’d like to do in particular is either expand to several calls to puthash if the macro arguments can be determined at expansion time, or otherwise expand to a cl-loop which processes elements of the list passed in at run-time. What exactly “determined at expansion time” means is a bit fuzzy to me (symbolp? Boundp? Or if there is something more I am missing). Thanks!


Solution

  • The code you quoted follows this structure:

    (defun fun (&rest args)
      (loop for (k v) on args by 'cddr do ...))
    

    Let's assume first that you rename the function fun% instead, and define a macro named fun, which at first can be as simple as that:

    (defmacro fun (&rest args)
      `(fun% ,@args))
    

    Each time you invoke fun directly, you will know how many parameters you have:

    (fun :a 0 :b 1)
    

    what I’d like to do in particular is either expand to several calls to puthash if the macro arguments can be determined at expansion time, or otherwise expand to a cl-loop which processes elements of the list passed in at run-time.

    Syntactically, this is a call with 4 arguments, the macro can access it through args variable which is bound to (:a 0 :b 1). So there is no case where the macro doesn't know how many arguments are passed to the call. Only the function fun% can receive an arbitrary number of arguments when you call it using apply:

    (let ((list (list :a 0 :b 1)))
      (apply 'fun% list))
    

    But macros cannot be applied like that (you generally don't invoke the macro-function yourself, the macroexpansion mechanism do it for you).

    The situation would be different if args was not a &rest argument, if you had instead:

    (defmacro fun (args)
      ....)
    

    Here you could only invoke fun with exactly one argument:

    (fun <list>)
    

    You have two common situations, both of which allow you to either optimize during expansion or defer to foo%:

    1. You want fun to have the same evaluation mechanism as usual functions, ie. you want the <list> expression to be evaluated as-is at runtime, and then use the value to populate the list. If you do so, then you can optimize for some known cases, like the list being constant (it is a quoted list). You will have to defer to a call to foo% anytime you don't know if args is a constant list or not.

    2. You are defining a special form where standard evaluation rules no longer apply. For example, you decide that the set of keys is probably always the same, and only the values need to be evaluated. You specify your own grammar:

       ARGS ::= SYMBOL | KW-LIST
       KW-LIST := (KEYWORD FORM . KW-LIST) | NIL
      

      KEYWORD items are expected to be literal symbols, but FORM values must be injected in the generated code at the place where their value is needed (in the calls to puthash).

      Valid calls are:

       (foo ())
       (foo (:a 10 :b (+ 10 20) :c (* 5 30)))
       (foo my-args)
      

      Note how the list is not quoted, it is part of the syntax of foo, it can be parsed at runtime and you can know its size, the only unknonw things in the macro are the values associated with the fixed set of keys.

    Both are possible, you generally never need to call eval, because how could the compiler ever know what the values are going to be when the code is run, in a totally different environment? Consider this Common Lisp code:

    (progn
      (print "Enter a list: ")
      (let ((list (read)))
        (fun list)))
    

    If fun is a macro, you cannot know how many items the list has, this is something that will be known after the user answer the prompt.