sigildocs

Chapter 8: Macros

Macros let you extend the language by transforming code at compile time.

Why Macros?

Consider implementing when:

(when (> x 0)
  (display "positive")
  (do-something))

You can't write this as a procedure because all arguments are evaluated. With a macro, you control when and if arguments are evaluated.

Defining Macros with syntax-rules

(define-syntax when
  (syntax-rules ()
    ((when test body ...)
     (if test
         (begin body ...)
         #f))))

This says: When you see (when test body ...), replace it with (if test (begin body ...) #f).

Let's break it down:

  • define-syntax creates a macro
  • syntax-rules () defines pattern-based transformation (empty list = no keywords)
  • (when test body ...) is the pattern
  • (if test (begin body ...) #f) is the template

Pattern Matching in Macros

Literals

Match specific symbols:

(define-syntax my-if
  (syntax-rules (then else)
    ((my-if test then yes else no)
     (if test yes no))))

(my-if (> 5 3) then "big" else "small")
; => "big"

The symbols then and else must appear literally.

Ellipsis

The ... matches zero or more items:

> (define-syntax list-of
    (syntax-rules ()
      ((list-of item ...)
       (list item ...))))
> (list-of 1 2 3 4)
(1 2 3 4)
> (list-of)
()

In the template, item ... expands to all matched items.

Multiple Patterns

Handle different cases:

(define-syntax my-or
  (syntax-rules ()
    ((my-or) #f)
    ((my-or e) e)
    ((my-or e1 e2 ...)
     (let ((t e1))
       (if t t (my-or e2 ...))))))

Patterns are tried in order until one matches.

Practical Macro Examples

unless

(define-syntax unless
  (syntax-rules ()
    ((unless test body ...)
     (if (not test)
         (begin body ...)
         #f))))

and

(define-syntax and
  (syntax-rules ()
    ((and) #t)
    ((and e) e)
    ((and e1 e2 ...)
     (if e1 (and e2 ...) #f))))

let* (sequential binding)

(define-syntax let*
  (syntax-rules ()
    ((let* () body ...)
     (begin body ...))
    ((let* ((var val) rest ...) body ...)
     (let ((var val))
       (let* (rest ...) body ...)))))

case

(define-syntax case
  (syntax-rules (else)
    ((case key)
     #f)
    ((case key (else body ...))
     (begin body ...))
    ((case key ((datum ...) body ...) rest ...)
     (let ((tmp key))
       (if (memv tmp '(datum ...))
           (begin body ...)
           (case tmp rest ...))))))

Hygiene

Macros in Sigil are hygienic — they don't accidentally capture variables:

(define-syntax swap!
  (syntax-rules ()
    ((swap! a b)
     (let ((tmp a))
       (set! a b)
       (set! b tmp)))))

(define tmp 10)
(define x 1)
(define y 2)
(swap! x y)
;; tmp is still 10, not affected by macro's internal tmp

The tmp inside the macro doesn't clash with any tmp at the use site.

When to Use Macros

Use macros when you need to:

  1. Control evaluation: Delay or prevent evaluation of arguments
  2. Add new syntax: Create domain-specific constructs
  3. Avoid repetition: Generate repetitive code patterns
  4. Enforce patterns: Ensure certain code structures

Don't use macros when a procedure will do:

  • Macros are harder to debug
  • Macros don't compose as easily
  • Procedure calls are more flexible

Debugging Macros

Use macroexpand to see what a macro expands to:

> (macroexpand '(when (> x 0) (display "positive")))
(if (> x 0) (begin (display "positive")) #f)

This shows exactly what code your macro generates. Test with simple cases first:

> (macroexpand '(my-or))
#f

> (macroexpand '(my-or a))
a

> (macroexpand '(my-or a b c))
(let ((t a)) (if t t (my-or b c)))

If the expansion looks wrong, adjust your patterns and templates.

Advanced Example: Loop Macro

A simple loop construct:

(define-syntax loop
  (syntax-rules (for in do)
    ((loop for var in lst do body ...)
     (for-each (lambda (var) body ...) lst))))

(loop for x in '(1 2 3)
  do (println "~a" x))

A counting loop:

(define-syntax repeat
  (syntax-rules ()
    ((repeat n body ...)
     (let loop ((i 0))
       (when (< i n)
         body ...
         (loop (+ i 1)))))))

(repeat 3
  (println "Hello!"))

The Power of Quasiquote

When writing procedural macros (using lambda transformers), quasiquote is essential:

(define-syntax debug
  (lambda (form)
    (let ((expr (cadr form)))
      `(begin
         (display ',expr)
         (display " = ")
         (display ,expr)
         (newline)))))

(define x 42)
(debug (+ x 1))
;; Prints: (+ x 1) = 43

Best Practices

  1. Start simple: Begin with syntax-rules before procedural macros
  2. Test incrementally: Test each pattern as you add it
  3. Document patterns: Explain what each pattern matches
  4. Prefer procedures: Only use macros when necessary
  5. Keep it readable: Complex macros should have clear structure

Practice Exercises

  1. Write a while macro that loops while a condition is true.
  2. Write a dotimes macro: (dotimes (i 5) (display i)) prints 0-4.
  3. Write an assert macro that prints the expression if it fails.

What's Next

Let's learn about error handling and debugging.

Next: Error Handling →