3
votes

I am generating emacs elisp code from a clojure function. I originally started off using a defmacro, but I realized since I'm going cross-platform and have to manually eval the code into the elisp environment anyway, I can just as easily use a standard clojure function. But basically what I'm doing is very macro-ish.

I am doing this because my goal is to create a DSL from which I will generate code in elisp, clojure/java, clojurescript/javascript, and maybe even haskell.

My "macro" looks like the following:

(defn vt-fun-3 []
  (let [hlq "vt"]
    (let [
         f0 'list
         f1 '(quote (defun vt-inc (n) (+ n 1)))
         f2 '(quote (ert-deftest vt-inc-test () (should (= (vt-inc 7) 8))))]
      `(~f0 ~f1 ~f2)
      )))

This generates a list of two function definitions -- the generated elisp defun and a unit test:

(list (quote (defun vt-inc (n) (+ n 1))) (quote (ert-deftest vt-inc-test () (should (= (vt-inc 7) 8)))))

Then from an emacs scratch buffer, I utilize clomacs https://github.com/clojure-emacs/clomacs to import into the elisp environment:

(clomacs-defun vt-fun-3 casc-gen.core/vt-fun-3)
(progn
    (eval (nth 0  (eval  (read (vt-fun-3)))))
    (eval (nth 1  (eval  (read (vt-fun-3))))))

From here I can then run the function and the unit test:

(vt-inc 4)
--> 5
(ert "vt-inc-test")
--> t

Note: like all macros, the syntax quoting and escaping is very fragile. It took me a while to figure out the proper way to get it eval properly in elisp (the whole "(quote (list..)" prefix thing).

Anyway, as suggested by the presences of the "hlq" (high-level-qualifier) on the first "let", I want to prefix any generated symbols with this hlq instead of hard-coding it.

Unfortunately, when I use standard quotes and escapes on the "f1" for instance:

 f1 '(quote (defun ~hlq -inc (n) (+ n 1)))

This generates:

    (list (quote (defun (clojure.core/unquote hlq) -inc (n) (+ n 1))) 
(quote (ert-deftest vt-inc-test () (should (= (vt-inc 7) 8)))))

In other words it substitutes 'clojure.core/unquote' for "~" which is not what I want.

The clojure syntax back-quote:

f1 `(quote (defun ~hlq -inc (n) (+ n 1)))

doesn't have this problem:

(list (quote (casc-gen.core/defun vt casc-gen.core/-inc (casc-gen.core/n) (clojure.core/+ casc-gen.core/n 1))) (quote (ert-deftest vt-inc-test () (should (= (vt-inc 7) 8)))))

It properly escapes and inserts "vt" as I want (I still have to work out to concat to the stem of the name, but I'm not worried about that).

Problem solved, right? Unfortunately syntax quote fully qualifies all the symbols, which I don't want since the code will be running under elisp.

Is there a way to turn off the qualifying of symbols when using the syntax quote (back tick)?

It also seems to me that the syntax quote is more "capable" than the standard quote. Is this true? Or can you, by trickery, always make the standard quote behave the same as the syntax quote? If you cannot turn off qualification with syntax quote, how could I get this working with the standard quote? Would I gain anything by trying to do this as a defmacro instead?

The worst case scenario is I have to run a regex on the generated elisp and manually remove any qualifications.

2

2 Answers

3
votes

There is no way to "turn off" the qualifying of symbols when using syntax quote. You can do this however:

(let [hlq 'vt] `(~'quote (~'defun ~hlq ~'-inc (~'n) (~'+ ~'n 1))))

Which is admittedly pretty tedious. The equivalent without syntax quote is:

(let [hlq 'vt] (list 'quote (list 'defun hlq '-inc '(n) '(+ n 1))))

There is no way to get your desired output when using standard quote prefixing the entire form however.

As to the issue of using defmacro instead, as far as I understand your intentions, I don't think you would gain anything by using a macro.

1
votes

Based on the input from justncon, here is my final solution. I had to do a little extra formatting to get the string concat on the function name right, but everything was pretty much like he recommended:

(defn vt-gen-4 []
  (let [hlq 'vt]
   (let [
         f1 `(~'quote (~'defun ~(symbol (str hlq "-inc")) (~'n) (~'+ ~'n 1)))
         f2 `(~'quote (~'defun ~(symbol (str hlq "-inc-test")) () (~'should (~'= (~(symbol (str hlq "-inc")) 7) 8))))
         ]
     `(~'list ~f1 ~f2))))

What I learned:

  1. syntax quote is the way to go, you just have to know how to control unquoting at the elemental level.

  2. ~' (tilde quote) is my friend here. Within a syntax quote expression, if you specify ~' before either a function or var it will be passed through to the caller as specified.

Take the expression (+ 1 1)

Here is a synopsis of how this expression will expand within a syntax quote expression based on various levels of escaping:

(defn vt-foo []
  (println "(+ 1 1) -> " `(+ 1 1))      -->  (clojure.core/+ 1 1)
  (println "~(+ 1 1) -> " `~(+ 1 1))    -->  2
  (println "~'(+ 1 1) -> " `~'(+ 1 1))  --> (+ 1 1)
  )

The last line was what I wanted. The first line was what I was getting.

  1. If you escape a function then do not escape any parameters you want escaped. For instance, here we want to call the "str" function at macro expand time and to expand the variable "hlq" to it's value 'vt:

    ;; this works
    f1 `(quote (defun ~(str  hlq "-inc") ~hlq (n) (+ n 1)))

    ;; doesn't work if you escape the hlq:
    f1 `(quote (defun ~(str  ~hlq "-inc") ~hlq (n) (+ n 1)))

I guess an escape spans to everything in the unit your escaping. Typically you escape atoms (like strings or symbols), but if it's a list then everything in the list is automatically escaped as well, so don't double escape.

4) FWIW, I ended writing a regex solution before I got the final answer. It's definitely not as nice:

(defn vt-gen-3 []
  (let [hlq "vt"]
    (let
        [
         f0 'list
         f1 `(quote (defun ~(symbol (str  hlq "-inc")) (n) (+ n 1)))
         f2 '(quote (ert-deftest vt-inc-test () (should (= (vt-inc 7) 8))))
         ]
      `(~f0 ~f1 ~f2)
      ))
  )

;; this strips out any qualifiers like "casc-gen.core/"
(defn vt-gen-3-regex []
  (clojure.string/replace (str (vt-gen-3)) #"([\( ])([a-zA-Z0-9-\.]+\/)" "$1" ))
  1. Macro expansion is very delicate and requires lots of practice.