Macros & Modules
Macros
Sema supports two macro systems: procedural defmacro-style macros with quasiquoting, unquoting, and splicing, and R7RS pattern-based define-syntax / syntax-rules macros with automatic hygiene.
defmacro
Define a macro that transforms code at expansion time.
(defmacro unless2 (test . body)
`(if ,test nil (begin ,@body)))
(unless2 #f (println "runs!"))macroexpand
Inspect the expansion of a macro call without evaluating it.
(macroexpand '(unless2 #f (println "x")))gensym
Generate a unique symbol manually. For most macro use cases, prefer auto-gensym (foo#) instead.
(gensym "tmp") ; => tmp__42 (unique each call)Auto-gensym (foo#)
Inside a quasiquote template, any symbol ending with # is automatically replaced with a unique generated symbol. All occurrences of the same foo# within a single quasiquote resolve to the same gensym, ensuring consistency.
This prevents variable capture — a common bug where macro-introduced bindings accidentally shadow user variables.
;; Without auto-gensym — BUG if user has a variable named "tmp"
(defmacro bad-inc (x)
`(let ((tmp ,x)) (+ tmp 1)))
(let ((tmp 100))
(bad-inc tmp)) ; => 2, not 101! "tmp" is captured
;; With auto-gensym — always correct
(defmacro good-inc (x)
`(let ((tmp# ,x)) (+ tmp# 1)))
(let ((tmp 100))
(good-inc tmp)) ; => 101 ✓Rules:
- Same
foo#in one quasiquote → same generated symbol - Each quasiquote evaluation → fresh symbols (no cross-expansion collisions)
- Outside quasiquote,
foo#is a regular symbol (no magic)
Best practice: Always use auto-gensym for bindings introduced by macros:
(defmacro swap! (a b)
`(let ((tmp# ,a))
(set! ,a ,b)
(set! ,b tmp#)))define-syntax / syntax-rules
R7RS pattern-based macros. Instead of computing an expansion with quasiquote, you declare rewrite rules — each a (pattern template) pair. Rules are tried in order and the first matching pattern wins; ... (ellipsis) matches a sequence of forms.
(define-syntax my-or
(syntax-rules ()
((_) #f)
((_ e) e)
((_ e1 e2 ...)
(let ((t e1)) (if t t (my-or e2 ...))))))
(my-or #f #f 7) ; => 7The first argument to syntax-rules is a list of literal identifiers — symbols that must appear verbatim in the call rather than binding a pattern variable:
(define-syntax go
(syntax-rules (to)
((_ to x) (list :to x))
((_ x) (list :plain x))))
(go to 1) ; => (:to 1)
(go 2) ; => (:plain 2)Nested ellipsis in patterns lets you destructure binding-list shapes:
(define-syntax my-let
(syntax-rules ()
((_ ((name val) ...) body)
((lambda (name ...) body) val ...))))
(my-let ((a 1) (b 2)) (+ a b)) ; => 3Hygiene
syntax-rules templates are hygienic without manual gensyms. Hygiene is binder-directed: identifiers the template introduces as binders (the variables of a template's let, let*, letrec, lambda, define, do, or named let) are automatically alpha-renamed to a fresh symbol on every expansion — so they can never capture a user variable of the same name:
(define-syntax swap!
(syntax-rules ()
((_ a b)
(let ((tmp a)) (set! a b) (set! b tmp)))))
(define tmp 1)
(define x 2)
(swap! tmp x)
(list tmp x) ; => (2 1) — the template's tmp did not capture the user's tmpEvery other template identifier — free references to user-defined globals, builtins, and the macro's own name for recursion — is kept verbatim and resolves at the use site at runtime:
(define (double n) (* 2 n))
(define-syntax d (syntax-rules () ((_ x) (double x))))
(d 21) ; => 42macroexpand works on syntax-rules macros too and shows the renaming:
(macroexpand '(swap! x y))
;; => (let ((tmp__0 x)) (set! x y) (set! y tmp__0))Caveats (the hygiene is an approximation, not full R7RS referential transparency):
- Only template-introduced binders are renamed. The other direction isn't covered: if the use site shadows a global or special form that the template references freely, the template sees the shadowing binding.
- Templates support a single level of ellipsis; a template with ellipsis depth > 1 (
x ... ...) is rejected with a clear error rather than mis-expanded. - Like
defmacro,define-syntaxmust appear at the top level (or inside a top-levelbegin) to be visible to sibling forms — one nested inside a lambda orletbody is not. syntax-caseis not supported.
defmacro or syntax-rules?
syntax-rules— reach for it when the macro is a structural rewrite: it's declarative, hygiene is automatic, and the definition is portable R7RS.defmacro— reach for it when the expansion needs real computation (inspecting arguments, generating different shapes, calling functions at expansion time): the whole language is available. Use auto-gensym (foo#) for any bindings the expansion introduces.
Built-in Macros
Sema includes several macros that are auto-loaded at startup. These don't need to be defined or imported:
->,->>,as->,some->— Threading macroswhen-let,if-let— Conditional binding
See Special Forms for full documentation.
Metaprogramming
eval
Evaluate data as code.
(eval '(+ 1 2)) ; => 3read
Parse a string into a Sema value.
(read "(+ 1 2)") ; => (+ 1 2) as a list valueio/read-many
Parse a string containing multiple forms.
(io/read-many "(+ 1 2) (* 3 4)") ; => ((+ 1 2) (* 3 4))type
Return the type of a value as a keyword.
(type 42) ; => :int
(type 3.14) ; => :float
(type "hi") ; => :string
(type :foo) ; => :keyword
(type 'foo) ; => :symbol
(type '(1 2 3)) ; => :list
(type [1 2 3]) ; => :vector
(type {:a 1}) ; => :mapFor records, type returns the record type tag as a keyword (e.g. :point).
Type Conversion Functions
(string/to-symbol "foo") ; => foo
(keyword/to-string :bar) ; => "bar"
(string/to-keyword "name") ; => :name
(symbol/to-string 'foo) ; => "foo"Modules
module
Define a module with explicit exports.
;; math-utils.sema
(module math-utils
(export square cube)
(define (square x) (* x x))
(define (cube x) (* x x x))
(define (internal-helper x) x)) ; not exportedimport
Import a module from a file. Only exported bindings become available.
;; main.sema
(import "math-utils.sema")
(square 5) ; => 25
(cube 3) ; => 27