sigildocs

Macros Guide

This guide covers advanced macro techniques in Sigil.

When to Use Macros

Use macros when you need to:

  1. Control evaluation: Delay or prevent evaluation of arguments
  2. Add syntax: Create new language constructs
  3. Avoid repetition: Generate boilerplate code
  4. 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: 3

Guards 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 tmp

The 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:

FunctionDescription
syntax?Test if value is a syntax object
identifier?Test if value is an identifier
syntax-datumGet the wrapped datum
syntax->datumRecursively strip syntax
datum->syntaxCreate 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-temporariesCreate fresh hygienic identifiers
syntax-with-metadataCreate 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 result

Accumulating 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

  1. Macro expansion happens at compile time — no runtime cost for the transformation itself
  2. Generated code should be efficient — avoid generating redundant operations
  3. Inline where beneficial — macros can inline what procedures can't

Macro vs Procedure Comparison

AspectMacroProcedure
EvaluationControls when args evaluateArgs always evaluated
Compile timeExpanded at compile timeCalled at runtime
DebuggingHarder to debugEasier to debug
ComposabilityLimitedFull
Code sizeMay increaseShared code

Best Practices

  1. Document patterns: Show example uses in comments
  2. Keep it simple: Complex macros are hard to maintain
  3. Test thoroughly: Test all pattern combinations
  4. Prefer procedures: Use macros only when necessary
  5. Consider alternatives: Can lambda or 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)))