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.