Search code examples
macrosracketmetaprogramming

Macro producing `require` form does not create bindings


I have a wrapper around an API that uses nested modules, like this:

(module api racket
  (module a racket
    (module b racket
      (provide hi-there)
      (define hi-there "hello"))))

I use it by requiring submodules with a prefix, like this: (require (prefix-in "a:b:" (submod 'api a b)).

Now, I want to write a macro that would help me make the require statements more elegant. I came up with the following:

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

(define-syntax (require-api stx)
  (syntax-parse stx
    [(_ root:id p1:id ...)
      (splicing-let ([prefix (string->symbol (string-join (map (lambda (s) (symbol->string (syntax->datum s)))
                                                               (syntax-e #'(p1 ...)))
                                                          ":"
                                                          #:after-last ":"))])
        #`(require (prefix-in #,prefix (submod 'root p1 ...))))]))

Even though it gives me the exact form as I were entering manually, it does not in fact work. It does not create a binding for the require'd module:

> (require-api api a b)
> a:b:hi-there
a:b:hi-there: undefined;
 cannot reference an identifier before its definition
  in module: top-level
 [,bt for context]
> (define-syntax (show stx)
    (syntax-case stx ()
      [(_)
        (let ([main (local-expand #'(require-api api a b)
                                    'top-level
                                    (list #'require))]
              [goal (local-expand #'(require (prefix-in a:b: (submod 'api a b)))
                                    'top-level
                                    (list #'require))])
          (printf "expanded: ~s\n" (syntax->datum main))
          (printf "goal:     ~s\n" (syntax->datum goal))
          main)]))
> (show)
expanded: (require (prefix-in a:b: (submod (quote api) a b)))
goal:     (require (prefix-in a:b: (submod (quote api) a b)))
> (require (prefix-in a:b: (submod (quote api) a b)))
> a:b:hi-there
"hello"

I'm clearly missing something -- but what?


Solution

  • To quote the documentation of require:

    The lexical context of the module-path form determines the context of the introduced identifiers

    In your macro, (submod 'root p1 ...) is the module-path. Since you construct it in the macro require-api, its lexical context belongs to the macro. But to make it import identifiers correctly, its lexical context should belong to the original macro invocation site (as if you wrote it in the original location yourself).

    An easy fix is to do the following manipulation:

    (datum->syntax this-syntax (syntax-e #'(submod 'root p1 ...)))
    

    Step-by-step explanation:

    1. #'(submod 'root p1 ...) is what you want, though it has a wrong lexical context.
    2. (syntax-e #'(submod 'root p1 ...)) extracts the list form from the syntax list.
    3. (datum->syntax this-syntax (syntax-e #'(submod 'root p1 ...))) packages the list form into a syntax list again, but with this-syntax's lexical context, which belongs to the original macro invocation site.

    Here's a full program:

    #lang racket
    
    (require (for-syntax racket/string
                         syntax/parse))
    
    (define-syntax (require-api stx)
      (syntax-parse stx
        [(_ root:id p1:id ...)
         
         #:with prefix
         (string->symbol
          (string-join (map (λ (s) (symbol->string (syntax-e s)))
                            (attribute p1))
                       ":"
                       #:after-last ":"))
         
         #:with mp
         (datum->syntax this-syntax (syntax-e #'(submod 'root p1 ...)))
         
         #'(require (prefix-in prefix mp))]))
    
    (module api racket
      (module a racket
        (module b racket
          (provide hi-there)
          (define hi-there "hello"))))
    
    (require-api api a b)
    a:b:hi-there ;=> "hello"