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.