2
votes

I want to make a macro, which behaviour will depend from one of it's arguments. For example:

(defclass myvar ()
  ((l :initarg :l
      :reader l)))

(defparameter mv1 (make-instance 'myvar :l 10))

(defmacro mac1 (v)
  `(progn
     ,@(loop for x upto (eval `(l ,v)) collect `(format t "~A~%" ,x))))

(mac1 mv1)

1
2
3
4
5
6
7
8
9
10

This works fine. But if I'm trying to do the same with local variable:

(let ((mv2 (make-instance 'myvar :l 10)))
  (mac1 mv2))

I'm getting the error:

 The variable MV2 is unbound.
 It is a local variable not available at compile-time.

Is there a way to evaluate local variable in macro?

1
Imagine mv2 is in fact a value obtained by process some value given by a call to (read): at the moment you expand the macro, most likely during compilation, in your compilation environment, you do not know which value the user will eventually enter at runtime, when the code actually runs. It could be different each time it is run. So no, macros are for expanding code, you cannot access the values that the code will produce once run. Your example seems a bit artificial so I don't know what to propose as a different implementation.coredump
The example is vary artificial, because the real core is rather big and complicated. The code is a set of macros, which must generate a code on different language. I want to make a variable and generate a loop basing on the variable dimensions, which can be defined by expressions, not just plain numbers: (let ((v1 (make-var (100 100)))) (loop-on v1 (some-code-here))), which must produce (in C, for example): {for(i=0;i<100;i++){for(j=0;j<100;j++){some code here}}}. The (some-code-here) must be evaluated after loop generation to know loop variables, so I need a macro.Pavel
macros compute with code, not with runtime valuesRainer Joswig

1 Answers

2
votes

Your goal is to compile some code into another language. There is hardly any need for macro to do that, you take a structured expression and produces another kind of tree, or directly emit code in a foreign language as string.

What is a bit confusing is that your code looks like Lisp, but does not really work like Lisp, since you want to have loop-on be able to inspect its lexical environment to know what compile-time binding is in place for some symbol. This is not specified in the standard, and even if you can find a way in your implementation to access that, using for example the &environment keyword in macros, then the solution would not be portable.

In any case, you'll have to specify how to translate Lisp forms as C, which means you have to implement an interpreter (in the general meaning of it, i.e. give an interpretation to code).

One simple way, if you have simple requirements, is to make make-var and loop-on constructors for data.

Let's define var and loop-on structs:

 (defstruct (var (:constructor make-var (x y))) x y)
 (defstruct loop-on var code)

Simple structures

As I don't know yet how you want to exploit the body, let's store it as-is. That part necessitates a macro, but until you specify how you compile the body, this won't be very useful.

(defmacro loop-on (v &body body)
  `(make-loop-on :var ,v :code ',body))

Now, your code can be evaluated as Lisp code:

(let ((v1 (make-var 100 100)))
  (loop-on v1 (some-code-here)))

And the resulting value is:

#S(LOOP-ON :VAR #S(VAR :X 100 :Y 100) :CODE ((SOME-CODE-HERE)))

Now, your compiler can expand this loops as C code, provided the :CODE slot satisfies the subset of Lisp you want to support.

A generic interpreter

For larger projects, you want to define a code walker that knows how to interpret the code. Let's define 6 generic functions, as follows:

(defgeneric interpret-let (interpreter env bindings code))
(defgeneric interpret-body (interpreter env forms))
(defgeneric interpret-loop (interpreter env var forms))
(defgeneric interpert-lisp (interpreter env form))

(defgeneric interpret (interpreter env code)
  (:method (i env code)
    (optima:ematch code
      ((list* 'let bindings code)
       (interpret-let i env bindings code))
      ((list 'make-var x y)
       (make-var x y))
      ((list* 'loop-on v code)
       (interpret-loop i env v code))
      ((list 'lisp form)
       (interpret-lisp i env form)))))

The code depends on optima for pattern matching. The input code is matched against known syntax and the behavior is delegated to generic functions, which are dispatched according the to type of interpreter you are building;

Then env environment variable is a lexical environment, it can hold bindings for variable names, functions, types, etc. You could dispatch on env too but here we assume this is an association list from symbols to values. Here is how you augment the environment for a set of given bindings, each of them being a list mapping a symbol to a form:

(defgeneric augment-env (interpreter env bindings)
  (:method (i env bindings)
    (nconc (loop for (n v) in bindings
                 collect (cons n (interpret i env v)))
           env)))

An evaluator

Here I specialize the methods for an interpreter named :eval. Notice that we don't match against a class, but against a keyword. For more sophisticated cases you can store some state in the interpreter.

(defmethod interpret-body ((i (eql :eval)) e (form cons))
  (destructuring-bind (form . rest) form
    (cond
      (rest (interpret i e form)
            (interpret-body i e rest))
      (t (interpret i e form)))))

(defmethod interpret-let ((i (eql :eval)) env bindings code)
  (interpret-body i (augment-env i env bindings) code))

(defmethod interpret-lisp ((i (eql :eval)) env form)
  (eval `(let ,(loop for (a . b) in env collect (list a b))
           (declare (ignorable ,@(mapcar #'car env)))
           ,form)))

(defmethod interpret-loop ((i (eql :eval)) e v body)
  (let ((var (cdr (assoc v e))))
    (dotimes (ii (var-x var))
      (dotimes (jj (var-y var))
        (interpret-body i
                        (acons 'i ii (acons 'j jj e))
                        body)))))

With the above definitions, you can interpret your code as follows:

(interpret :eval
           nil
           '(let ((v1 (make-var 10 5)))
             (loop-on v1 (lisp (print (list i j))))))

This performs the double loop and print values to the standard output.

A code expander

Let's now write a code interpreter that expands the input form as a different kind of language. In practice I would write here code that represents C code, if the target language is C. Then the resulting code could be pretty-printed as C (this tends to be better than directly writing C).

(defmethod interpret-let ((i (eql :expand)) e bindings code)
  (loop for (a b) in bindings
        for v = (interpret i e b)
        if (typep v 'var)
        collect (cons a v) into new-env
        else
        collect (list a v) into new-bindings
        finally
           (return
             `(c/block
                ;; remove VAR instances (they are expanded at compile-time)
                (c/declare ,new-bindings)
                ,@(interpret-body i (append new-env e) code)))))
    
(defmethod interpret-body ((i (eql :expand)) e code)
  (loop for f in code collect (interpret i e f)))

(defmethod interpret-loop ((i (eql :expand)) e v body)
  (let ((var (cdr (assoc v e))))
    (assert var)
    `(c/for (i (< i ,(var-x var)) (++ i))
       (c/for (j (< j ,(var-y var)) (++ j))
            ,@(interpret-body i
                              (augment-env i e `((i i) (j j)))
                              body)))))

(defmethod interpret ((i (eql :expand)) e (code symbol))
  code)

The last part that invokes Lisp is probably not important for your case, but let's say that we have a way to call a Lisp interpreter in our C code that can take an list of variable bindings as a parameter.

(defmethod interpret-lisp ((i (eql :expand)) e form)
  `(c/lisp-lexenv-eval ,(princ-to-string form)
                       ,(loop for (a . b) in e
                              when (symbolp b)
                              append (list (string a) b))))

With this expander, the result would be different:

(interpret :expand
           nil
           '(let ((v1 (make-var 10 5)))
             (loop-on v1 (lisp (print (list i j))))))

The generated code is:

(C/BLOCK (C/DECLARE NIL)
  (C/FOR (I (< I 10) (++ I))
         (C/FOR (J (< J 5) (++ J))
                (C/LISP-LEXENV-EVAL "(PRINT (LIST I J))"
                                    ("I" I "J" J)))))

With a pretty printer, you could then emit the corresponding C code.