evaluate an argument of Common Lisp macro

回眸只為那壹抹淺笑 提交于 2021-02-11 06:30:44

问题


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:


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.



来源:https://stackoverflow.com/questions/66049149/evaluate-an-argument-of-common-lisp-macro

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!