Macro expansion in Hy-based custom languages
This seems like a very obvious thing to do, but I could not find a simple example anywhere.
Suppose we want to define a very simple S-expression-based language, with no variables or functions, just literals and a single core form – print:
(print [1 2 3])
Outputting:
'[1 2 3]
However, to allow the user to abstract, we want to permit the usage of Hy macros – with the full Hy language available at expansion time:
(defmacro iota [max] (list (range 1 (+ 1 max))))
; pointless macro just to demonstrate repeated expansion
(defmacro identity [expr] expr)
(print (identity (iota 3)))
Output:
'[1 2 3]
Here is the Hy source of an interpreter for this language. This is as simple as I managed to get:
#!/usr/bin/env hy
(import hyrule)
(import os)
(import sys)
(import types)
(with [f (open "minimal.hy")] (do
    (setv module (types.ModuleType "minimal"))
    ; without the following, evaluation of defmacro triggers an error
    (setv (get sys.modules module.__name__) module)
    (setv compiler (hy.compiler.HyASTCompiler module module.__name__))
    (for [form (hy.read-many f)]
        ; expand macros -- this includes processing of "defmacro" itself
        (setv exp (hyrule.macrotools.macroexpand-all
                :form form
                :ast-compiler compiler
                ))
        ;(print "EVAL" (hy.repr exp))
        ; evaluate form
        (cond
            (= (get exp 0) (hy.models.Symbol "defmacro"))
                ; at evaluation time ignore defmacro
                None
            (= (get exp 0) (hy.models.Symbol "print"))
                ; this is our core form
                (print (hy.repr (get exp 1)))
            True
                (raise (Exception "invalid form"))
        )
    )
))
One could argue that this is a stupid thing to do, that I should have simply defined my print as a new macro, and used the standard hy.eval. Well, what if my set of core forms is not known ahead of time? Consider another language, one where each core form executes a shell command of the same name:
(defmacro get-filename [] "hello.txt")
(echo "Hello" "World" ">" (get-filename))
(cat (get-filename))
(uname "-a")
Well, with this structure, you can do that! The evaluation block just changes to this:
        ; evaluate form
        (if (!= (get exp 0) (hy.models.Symbol "defmacro"))
            (os.system (.join " " (list exp)))
            None
        )
This seems quite useful, doesn’t it – imagine the possibilities! I would be curious to see the Racket equivalent, since the juggling of modules and scopes seemed quite a bit more involved there. On the other hand, Racket has first-class custom language support, which might help quite a bit. And then there is ee-lib, which I have yet to explore.