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.