Using syntax/loc

:: Racket, macros

There’s a nuance to syntax/loc. The documentation says, emphasis mine:

Like syntax, except that the immediate resulting syntax object takes its source-location information from the result of stx-expr (which must produce a syntax object), unless the template is just a pattern variable, or both the source and position of stx-expr are #f.

What does “immediate” mean here?

Let’s back up. Say that we1 are writing a macro to both define and provide a function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#lang racket

(require (for-syntax syntax/parse))

(define-syntax (defp stx)
  (syntax-parse stx
    [(_ (id:id arg:expr ...) body:expr ...+)
     #'(begin
         (define (id arg ...)
           body ...)
         (provide id))]))

(defp (f x)
  (/ 1 x))

(f 1)
;; => 1

A macro must return one syntax object, but we want to return both a define and a provide. So we do the usual thing: We wrap them up in a begin.

Everything fine so far.

Now let’s say we call f and it causes a runtime error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#lang racket

(require (for-syntax syntax/parse))

(define-syntax (defp stx)
  (syntax-parse stx
    [(_ (id:id arg:expr ...) body:expr ...+)
     #'(begin
         (define (id arg ...)
           body ...)
         (provide id))]))

(defp (f x)
  (/ 1 x))

(f 1)
; 1

(f 0)
; /: division by zero
; Context:
;  /tmp/derp.rkt:9:9 f
;  derp [running body]

See the problem? The error message points to line 9 — inside the defp macro. That’s not helpful. We want it to say line 13 — where we use defp to define the f function.

Fortunately, we’ve heard of syntax/loc. Instead of specifying the macro template using #' a.k.a. syntax, we can use syntax/loc. Easy. So we update our macro:

1
2
3
4
5
6
7
8
(define-syntax (defp stx)
  (syntax-parse stx
    [(_ (id:id arg:expr ...) body:expr ...+)
     (syntax/loc stx  ;; <-- new
       (begin
         (define (id arg ...)
           body ...)
         (provide id)))]))

But that still doesn’t work. The error message still points to the macro.

Huh. So we try other values, like (syntax/loc id . . .). But still no joy.

It turns out this is where that word “immediate” matters: syntax/loc is setting the source location for the entire (begin . . . ) syntax object — but not necessarily the pieces inside it. That’s why the define isn’t getting the source location we’re supplying.

The solution? Use syntax/loc directly on the define piece:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#lang racket

(require (for-syntax syntax/parse))

(define-syntax (defp stx)
  (syntax-parse stx
    [(_ (id:id arg:expr ...) body:expr ...+)
     #`(begin
         #,(syntax/loc stx  ;; <-- new
             (define (id arg ...)
               body ...))
         (provide id))]))

(defp (f x)
  (/ 1 x))

(f 1)
; 1

(f 0)
; /: division by zero
; Context:
;  /tmp/derp.rkt:14:0 f
;  derp [running body]

And now the error message points to f on line 14. \o/


There’s also a quasisyntax/loc.

Cheat sheet:

Default Source location
Plain #' a.k.a. syntax syntax/loc
Quasi #` a.k.a. quasisyntax quasisyntax/loc

To anticipate a comment: Yes, I know. I ought to add this to Fear of Macros, too.

  1. The “we” in this blog post was “me” yesterday. Big thanks to Eric Dobson for pointing this out on #racket IRC.