Custom Macros Expander
Written by Dominik Pantůček on 2026-05-21
racketA custom splicing expander alone is not very useful on its own. Without a way to define custom macros that will be expanded not by the default Racket expander but by our custom expander it cannot do much. Now it is time to define an expander for custom macros.
The first step is to create a wrapper for the syntax transformer. In Racket we can use struct for such wrapper. With the right property it can be used as procedure, which - at phase 1 - is the typical syntax transformer usage.
(begin-for-syntax
(struct expmacro (proc)
#:property prop:procedure (struct-field-index proc)))
This definition allows the transformer to be expanded by the Racket expander should we need to leave it alone. Now we only need some means of defining the transformer binding. A simple syntax should suffice:
(define-syntax (define-macro stx)
(syntax-parse stx
((_ (id arg ...) body ...)
#'(define-syntax id
(expmacro
(lambda (sty)
(syntax-parse sty
((_ arg ...)
#'(begin
body ...)))))))))
Now the next step would be to get the ability to match these macros in the code. A simple syntax class can do the trick in this case. Now it becomes clear why we introduced the wrapping - it allows for checking the type of the value and if it matches our wrapper, we know it is our special macro:
(begin-for-syntax
(define-syntax-class mexpr
#:description "possible expander macro use"
#:attributes (transformer)
(pattern (id:id arg ...)
#:do ((define s (syntax-local-value #'id (lambda () #f))))
#:when (expmacro? s)
#:attr transformer (expmacro-proc s))
(pattern ex
#:attr transformer #f)))
Actually this syntax class does not only match our wrapped transformers but it also allows for distinguishing whether given expression is or is not such macro dynamically. Why is this little distinction so important? It allows a pretty straightforward implementation of a macro expander:
(begin-for-syntax
(define (expand-macros stx)
(syntax-parse stx
((me:mexpr ...)
#:with (ex ...) (for/list ((e (in-list (attribute me)))
(t (in-list (attribute me.transformer))))
(if t (t e) e))
(if (for/or ((t (in-list (attribute me.transformer)))) t)
#'(ex ...)
stx)))))
This procedure takes a syntax of a single S-Expression containing (possibly many) sub-expresions that may or may not be our special wrapper macro. If it is our special macro, we expand it immediately and if it is not, we keep it intact. Also if no macros would be expanded, we return the original syntax - so that a multi-pass expander can rely on detecting expansion fixed-point using eq?.
That was easy, wasn't it? Hope you will start implementing your own macro expanders now and see you next time for some more!