Macros Guide
This guide covers advanced macro techniques in Sigil.
When to Use Macros
Use macros when you need to:
- Control evaluation: Delay or prevent evaluation of arguments
- Add syntax: Create new language constructs
- Avoid repetition: Generate boilerplate code
- Enforce patterns: Ensure consistent code structure
Do NOT use macros when:
- A procedure would work
- The transformation is simple
- Composability is important
syntax-rules Patterns
Basic Pattern Matching
(define-syntax swap!
(syntax-rules ()
((swap! a b)
(let ((tmp a))
(set! a b)
(set! b tmp)))))Literals
Match specific keywords:
(define-syntax for
(syntax-rules (in)
((for x in lst body ...)
(for-each (lambda (x) body ...) lst))))
(for item in '(1 2 3)
(display item))Ellipsis Patterns
Match zero or more items:
(define-syntax my-list
(syntax-rules ()
((my-list item ...)
(list item ...))))
(my-list 1 2 3 4) ; => (1 2 3 4)
(my-list) ; => ()Multiple Clauses
Handle different forms:
(define-syntax assert
(syntax-rules ()
((assert expr)
(assert expr "Assertion failed"))
((assert expr message)
(unless expr
(error message 'expr)))))Recursive Patterns
Macros can expand recursively:
(define-syntax let*
(syntax-rules ()
((let* () body ...)
(begin body ...))
((let* ((var val) rest ...) body ...)
(let ((var val))
(let* (rest ...) body ...)))))Procedural Macros with syntax-case
For complex transformations, syntax-case provides pattern matching on syntax objects with full access to Scheme:
(define-syntax my-when
(lambda (stx)
(syntax-case stx ()
((_ test body ...)
#'(if test (begin body ...))))))The #' is shorthand for (syntax ...) and creates syntax templates with proper hygiene.
Pattern Variables and Templates
Pattern variables from syntax-case are substituted into #' templates:
(define-syntax debug-expr
(lambda (stx)
(syntax-case stx ()
((_ expr)
#'(let ((result expr))
(display 'expr)
(display " = ")
(display result)
(newline)
result)))))
(debug-expr (+ 1 2))
; Prints: (+ 1 2) = 3
; Returns: 3Guards in syntax-case
Add conditions to pattern clauses:
(define-syntax assert-type
(lambda (stx)
(syntax-case stx ()
((_ expr pred)
(identifier? #'pred) ; guard: pred must be an identifier
#'(let ((v expr))
(unless (pred v)
(error "Type assertion failed")))))))Quasiquote-Based Macros
For simpler cases, lambda with quasiquote works:
(define-syntax with-timing
(lambda (form)
(let ((start (gensym))
(result (gensym))
(body (cdr form)))
`(let ((,start (current-milliseconds)))
(let ((,result (begin ,@body)))
(display "Time: ")
(display (- (current-milliseconds) ,start))
(display "ms")
(newline)
,result)))))Use gensym to generate unique names and avoid variable capture.
Hygiene
Sigil macros are hygienic, meaning they respect lexical scope and don't accidentally capture or shadow variables. Hygiene has three key properties:
1. Introduced Bindings Don't Capture
Variables introduced by a macro don't capture identically-named variables at the use site:
(define-syntax swap!
(syntax-rules ()
((swap! a b)
(let ((tmp a))
(set! a b)
(set! b tmp)))))
(let ((tmp 100))
(let ((x 1) (y 2))
(swap! x y)
tmp)) ; => 100, not affected by macro's tmpThe tmp inside the macro is a different binding from the user's tmp.
2. Free Identifiers Resolve at Definition Site
When a macro uses a helper function or variable, it refers to the binding visible where the macro was defined, not where it's used:
(define helper (lambda (x) (+ x 1)))
(define-syntax use-helper
(syntax-rules ()
((use-helper v) (helper v))))
;; Even if we shadow 'helper' at the use site:
(let ((helper (lambda (x) (* x 2))))
(use-helper 5)) ; => 6 (uses original helper, not shadowed one)3. Literal Comparison Uses Binding Context
In syntax-rules and syntax-case, literals are compared by their binding, not just their name. A locally-bound identifier won't match a literal:
(define-syntax check-keyword
(syntax-rules (keyword)
((check-keyword keyword) 'matched)
((check-keyword _) 'not-matched)))
(check-keyword keyword) ; => matched
(let ((keyword 42))
(check-keyword keyword)) ; => not-matched (different binding)Advanced: The (sigil syntax) Module
For advanced macro authors who need fine-grained control over syntax objects, import (sigil syntax):
(import (sigil syntax))This module exports:
| Function | Description |
|---|---|
syntax? | Test if value is a syntax object |
identifier? | Test if value is an identifier |
syntax-datum | Get the wrapped datum |
syntax->datum | Recursively strip syntax |
datum->syntax | Create syntax with lexical context |
bound-identifier=? | Compare if identifiers would bind the same variable |
free-identifier=? | Compare if identifiers refer to the same binding |
generate-temporaries | Create fresh hygienic identifiers |
syntax-with-metadata | Create syntax with updated metadata |
Example: Using generate-temporaries
Create unique identifiers for macro-generated bindings:
(import (sigil syntax))
;; Generate 3 fresh identifiers
(define temps (generate-temporaries '(a b c)))
;; => (#<syntax tmp-0> #<syntax tmp-1> #<syntax tmp-2>)
;; Each call generates new unique names
(map syntax-datum (generate-temporaries '(x y)))
;; => (tmp-3 tmp-4)Example: Comparing Identifiers
(import (sigil syntax))
(let ((id1 (datum->syntax #f 'x))
(id2 (datum->syntax #f 'x))
(id3 (datum->syntax #f 'y)))
(bound-identifier=? id1 id2) ; => #t (same marks)
(bound-identifier=? id1 id3)) ; => #f (different names)Debugging Macros
Using macroexpand
See what a macro expands to:
(macroexpand '(when #t (display "hello")))
;; => (if #t (begin (display "hello")))Manual Expansion
Test transformations by printing:
(define-syntax my-macro
(lambda (form)
(let ((expansion `(transformed ,@(cdr form))))
(display "Expanding to: ")
(write expansion)
(newline)
expansion)))Step-by-Step
For complex macros, build incrementally:
;; Start simple
(define-syntax loop
(syntax-rules ()
((loop body)
(let f () body (f)))))
;; Add features one at a time
(define-syntax loop
(syntax-rules (while)
((loop body)
(let f () body (f)))
((loop while test body ...)
(let f ()
(when test
body ...
(f))))))Common Patterns
Anaphoric Macros
Introduce implicit bindings:
(define-syntax aif
(syntax-rules ()
((aif test then)
(let ((it test))
(if it then #f)))
((aif test then else)
(let ((it test))
(if it then else)))))
(aif (find-user "alice")
(display (user-name it))) ; 'it' bound to resultAccumulating Macro
Build structures incrementally:
(define-syntax define-enum
(lambda (form)
(let ((name (cadr form))
(values (cddr form)))
(let loop ((vals values) (n 0) (defs '()))
(if (null? vals)
`(begin ,@(reverse defs))
(loop (cdr vals)
(+ n 1)
(cons `(define ,(car vals) ,n) defs)))))))
(define-enum color red green blue)
;; Expands to:
;; (begin
;; (define red 0)
;; (define green 1)
;; (define blue 2))Wrapper Macros
Add behavior around expressions:
(define-syntax with-database
(syntax-rules ()
((with-database db body ...)
(let ((conn (db-connect db)))
(let ((result (begin body ...)))
(db-close conn)
result)))))Performance Considerations
- Macro expansion happens at compile time — no runtime cost for the transformation itself
- Generated code should be efficient — avoid generating redundant operations
- Inline where beneficial — macros can inline what procedures can't
Macro vs Procedure Comparison
| Aspect | Macro | Procedure |
|---|---|---|
| Evaluation | Controls when args evaluate | Args always evaluated |
| Compile time | Expanded at compile time | Called at runtime |
| Debugging | Harder to debug | Easier to debug |
| Composability | Limited | Full |
| Code size | May increase | Shared code |
Best Practices
- Document patterns: Show example uses in comments
- Keep it simple: Complex macros are hard to maintain
- Test thoroughly: Test all pattern combinations
- Prefer procedures: Use macros only when necessary
- Consider alternatives: Can
lambdaor higher-order functions work?
Example: Domain-Specific Language
Build a simple test DSL:
(define-syntax test-suite
(syntax-rules (test)
((test-suite name
(test test-name test-body ...) ...)
(begin
(display "Suite: ")
(display name)
(newline)
(run-test test-name (lambda () test-body ...)) ...))))
(define (run-test name thunk)
(display " ")
(display name)
(display ": ")
(if (thunk)
(display "PASS")
(display "FAIL"))
(newline))
(test-suite "Math tests"
(test "addition" (= (+ 1 2) 3))
(test "subtraction" (= (- 5 3) 2)))