Question About Macro Bindings

Hello :wave:! I am tying to write 2 simple macros where one renames a identifier by appending $ to the beginning and end. The other returns the newly created identifiers in a list.

#lang racket
(require
 (for-syntax syntax/parse
             racket/syntax
             syntax/stx))
(provide def id-list)

(define-syntax (def stx)
  (syntax-parse stx
    [(_ id:id val:expr)
     #:with new-id (format-id #'id "$~a$" #'id)
     #`(define new-id val)]))

(define-syntax (id-list stx)
  (syntax-parse stx
    [(_ ids:id ...)
     #:with (new-ids ...) (stx-map (lambda (i) (format-id #`i "$~a$" i)) #`(ids ...))
     #`(list new-ids ...)]))

(def x 10)
(def y 20)
(id-list x y)

This works as expected when I call the macro from within the module that it was created, however when I try the run the code from another module or submodule I get an unbound identifier error. I have a feeling this has to do with binding and phase levels, but after reading the docs I am still confused. Any help is appreciated.

-Josh

Example:

(module+ test
  (def x 10)
  (def y 20)

  (id-list x y))


; $x$: unbound identifier
;   in: $x$
;   context...:
;    #(1376184 macro) #(1376187 local) #(1376188 intdef) [common scopes]
;   other binding...:
;    #($x$ #<module-path-index=(submod 'test[2358842] test)> 0)
;    #(1376160 module) [common scopes]
;   common scopes...:
;    #(1375539 module) #(1375546 module test) #(1376167 module test)

1 Like

In your id-list macro, you have:

#i`

in the call of format-id. You probably meant to write just i.

In def, you need to put #' in front of id because id is a syntax pattern. But in id-list, i is just a regular variable, so you should not put #' in front of it.

2 Likes

Thank you for the explanation!

Try something like this:

#lang racket
(provide def id-list)

(require (for-syntax syntax/parse racket/syntax syntax/stx))

(begin-for-syntax
  (define common-ctx #'common-context-for-def-and-id-list))

(define-syntax (def stx)
  (syntax-parse stx
    [(_ id:id val:expr)
     #:with new-id (syntax-local-introduce (format-id common-ctx "$~a$" #'id))
     #'(define new-id val)]))

(define-syntax (id-list stx)
  (syntax-parse stx
    [(_ ids:id ...)
     #:with (new-ids ...) (stx-map (λ (i) (format-id common-ctx "$~a$" i))
                                   #'(ids ...))
     #'(list new-ids ...)]))

;(def x 10) (def y 20) (id-list x y)

(module+ test
  (def x 10)
  (def y 20)

  (id-list x y))

The idea here is to make the common scope for identifiers in def and id-list explicit.

Roughly, every binding form and macro expansion creates a scope, and each fragment of syntax acquires a set of scopes that determines binding of identifiers within the fragment. In a language without macros, each scope set is identifiable by a single innermost scope. In a language with macros, identifiers acquire scope sets that overlap in more general ways.

When the identifier in id-list is "recreated" the part of the scope from the macro expansion of def is missing. The snippet above uses syntax-local-introduce to fix that.

1 Like

It seems like @sorawee's replay was able to fix the issue. Is there a added benefit to creating a common scope for identifiers as your suggesting?

Also would you mind elaborating (or pointing me to some racket docs) that discuss the below in more detail?

identifiers acquire scope sets that overlap in more general ways.

Nothing more that making the connection between the two macros explicit.

I can recommend:

1 Like

Awesome! I will take a look at it. Thanks for all your help!

1 Like