Cannot (require ...) in module inside macro

When using (require rackunit) in a macro, the identifiers are not properly imported, and I get this error:

test2.rkt:11:1: check-equal: unbound identifier
  in: check-equal
  #(169 11)

Minimal example:

#lang racket

(require (for-syntax syntax/parse))

(define-syntax (require-rackunit stx)
  (syntax-parse stx
    ((_)
     #'(require rackunit))))

(require-rackunit)
(check-equal 1 1)

The macro stepper confirms it was expanded correctly:

(module test2 racket (#%module-begin (require (for-syntax syntax/parse)) (define-syntax (require-rackunit stx) (syntax-parse stx ((_) #'(require rackunit)))) (require-rackunit) (check-equal 1 1)))

→  [Macro transformation]

(module test2 racket (#%module-begin (require (for-syntax syntax/parse)) (define-syntax (require-rackunit stx) (syntax-parse stx ((_) #'(require rackunit)))) (require rackunit) (check-equal 1 1)))

This is not limited to rackunit, it seems like racket cannot (require ...) anything inside a macro.

Two problems. The first one is that it should be check-equal?, not check-equal.

But fixing that will still result in the unbound id error. The documentation of require states:

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

The issue in your code is that inside a macro (e.g. require-rackunit), the lexical context is that of the macro, which is not the right lexical context to be used.

One possible fix is:

(define-syntax (require-rackunit stx)
  (syntax-parse stx
    ((_)
     #:with RACKUNIT (datum->syntax stx 'rackunit)
     #'(require RACKUNIT))))

This changes the lexical context of rackunit to the macro caller (stx), so your code will now work.

The code will fail again if you have another macro that calls require-rackunit, since the lexical context will be that of the outer macro. Arguably, that is the desirable result. But if you want that to work too, you need to keep threading the lexical context down, similar to what we are doing here.

Another alternative is to call syntax-local-introduce on the syntax object containing the require. (Not sure if that’s considered « good form ».)

One downside of syntax-local-introduce is that if require-rackunit is exported from a module, and then imported to be used in another module, it won’t work properly.

It’s unfortunate that the example of syntax-local-introduce in the documentation kinda implies that this exact problem can be solved by using syntax-local-introduce, while there’s a caveat like this.

Thanks @sorawee and @benknoble.

I'm trying to attach unit tests to a function definition, similar to the "where" keyword in the pyret language. This is where I'm at right now:

#!racket/base

(require (for-syntax syntax/parse racket/syntax racket/base))
(provide with-checks)

(define-syntax (with-checks stx)
  (syntax-parse stx
    ((_ def:expr checks:expr ...)
     (syntax-local-introduce #'(begin def
                                      (module+ test
                                        (require rackunit)
                                        checks ...))))))
   
(with-checks
  (define (f x) (+ x 1))
  (check-equal? (f 1) 2)
  (check-equal? (f 4) 6))

This works when I use with-checks in the same module, but not when I import with-checks in another module. I'm trying to convert it to use datum->syntax, and I understand that I need to bind the lexical scope of the "test" module to the "(require rackunit)", but not sure how to do it.

[EDIT: Indeed this is N/A; more posts arrived while I was writing it and hit send. I guess I'll keep it, on the tiny chance it's useful to someone else someday.]

@sorawee gave a great answer to your specific question, @axmx, about your minimal example, where you define a macro to do a require at each site where the macro is used.

But I've rarely seen that done. If you're sure that's what want to do, and you're already familiar with Racket -- great; ignore the rest of this. :smile:

More typically, you'd define some module, which provides your own definitions and/or requires and re-provides definitions from other modules.

Let's say my-test-utils.rkt:

#lang racket/base
(require rackunit)
(provide (all-from-out rackunit)
         my-extra-test-stuff)
(define (my-extra-test-stuff) _TBD_)

Now any other module can (require "my-test-utils.rkt") to get your mix of standard rackunit stuff plus your extra stuff.

You could even package this up as a lang or meta-lang, as described in "Languages as Dotfiles".

(Again apologies if you already know all this and want to do something else instead.)

Well, this works for me.

#lang racket

(module server racket
  (require (for-syntax syntax/parse racket/syntax racket/base))
  (provide with-checks)

  (define-syntax (with-checks stx)
    (syntax-parse stx
      ((_ def:expr checks:expr ...)
       #:with RACKUNIT (datum->syntax stx 'rackunit)
       #'(begin def
                (module+ test
                  (require RACKUNIT)
                  checks ...))))))

(require 'server)

(with-checks
    (define (f x) (+ x 1))
  (check-equal? (f 1) 2)
  (check-equal? (f 4) 6))

Keep in mind that Pyret’s function definition with a where clause does not faithfully translate into the test submodule. For one, a function definition with a where clause can appear at non-module context (e.g., nested within another function definition). But the expansion to the test submodule mandates that you must be at the module context. (You can probably use something like syntax-local-lift-module to workaround that though.)

Yes that works, once I remembered to correct the check-equal/check-equal? thing. :man_facepalming:

I only want to test top-level function definitions, so it's fine that the macro doesn't exactly match pyret where behavior.

Thanks all!

Wait, really? I wasn’t aware of that drawback. Does the datum->syntax manipulation not have it? I suppose not, since you manipulate the contexts explicitly…

Consider local-require to get closer to Pyret.

My understanding is that datum->syntax-ish or other related operations is the way to go precisely because of the issue with modules. syntax-local-introduce flips the fresh macro scope and the use-site scope but doesn't take care of module scopes.