Hey @Henrik_Larsson ,
This is quite an interesting topic! Youāre basically writing a little compiler.
The thing I find counterintuitive, yet itās true, is that an interpreter and a compiler can have a very similar structure. Both are usually recursive functions with a big conditional, one branch for each kind of expression you have. The difference is that interpreters return the answer, while compilers return the code that will calculate the answer.
I like to take advantage of that and develop my language in an interpreter first, because itās typically less tricky. I can write test cases for it and everything. Then, when Iāve got it where I want it, I can copy the code and convert it to the compiler version. The other cool thing is that you can then compare the outputs of both. They should be the same.
Iāll start with the interpreter for your language. Hereās the example AST you provided:
[:not [:or [:term "a" 3] [:term "a" 4]]]
Hereās an interpreter for this and and
. Iām assuming :term
means to compare for equality:
(defn interpret [[op & [a b]]]
(case op
:term (= a b)
:not (not (interpret a))
:or (or (interpret a) (interpret b))
:and (and (interpret a) (interpret b))))
Iāll leave the case of more than two arguments to you. But now I can convert it into a compiler. Iāll make it return Clojure code:
(defn compile [[op & [a b]]]
(case op
:term `(= ~a ~b)
:not `(not ~(compile a))
:or `(or ~(compile a) ~(compile b))
:and `(and ~(compile a) ~(compile b))))
That returns Clojure code. You can convert it to a function like this:
(defn make-function [expr]
(eval `(fn [] ~(compile expr))))
This compiler returns Clojure code, so it needs an eval
to run it. Since itās wrapped in a fn
form, the eval simply compiles it into a function. But we can take that and make the compiler work directly on functions. That way, we donāt need to work with code and eval
. Itās still a compiler, so it has the same structure:
(defn compile2fn [[op & [a b]]]
(case op
:term #(= a b)
:not (let [f (compile2fn a)]
#(not (f)))
:or (let [fa (compile2fn a)
fb (compile2fn b)]
#(or (fa) (fb)))
:and (let [fa (compile2fn a)
fb (compile2fn b)]
#(and (fa) (fb)))))
Itās still quite fast, but it doesnāt work with code. Code writing code can get hairy.
I think I prefer the compiler to functions. I like to reserve returning code for macros. And I reserve macros for stuff that has to run at compile time.
Rock on!
Eric