UP | HOME

subtle racket macro

Racket macro is a powerful and great tool to create a new syntax form for existing language. Most of the time it just works as expected, but sometimes we would meet some subtle bugs from it. I would explain a certain case of this situation, then tell the possible solution we could apply, and how to detect problems when we meet such a weird sample. The story starts from another dependent-type language I recently build, I usually avoid infix syntax in racket language, but this time infix syntax is better so I pick it. To claim a variable binding with type I use :.

a : Nat

To bind a value with a variable I use =.

a = zero

Here is the minimal reproducable sample.

(define (foo stx)
  (syntax-case stx (=)
    [(name = expr)
     `(,(syntax-e #'name) = ,(syntax-e #'expr))]))

(foo #'(a = 1))

This produces correct stuff locally, the reason why I didn't find the bug at first, but when providing module language we usually would put something like the following code.

(provide (except-out (all-from-out racket) #%module-begin #%top-interaction)
         (rename-out [module-begin #%module-begin]
                     [top-interaction #%top-interaction]))

(define-syntax (module-begin stx)
  ...)
(define-syntax (top-interaction stx)
  ...)
(module reader syntax/module-reader
  typical)

The above program overwrites #%module-begin and #%top-interaction to help module language works for file(a module) and REPL(interaction). For convenience, all from racket usually re-export, so we can have provide, require, and everything we still would like to have. However, a = zero failed since = is defined in racket/base, we can imagine that syntax-case is trying to pattern matched #<procedure:=> with =. The simplest solution is (except-out (all-from-out racket) #%module-begin #%top-interaction =) let = is just another symbol, this works and apply for now, but I'm going to dig more solution here. Back to our reproduce sample, and add a variant foo.

(define (foo-p stx)
  (syntax-parse stx
    #:literals (=)
    [(name = expr)
     `(,(syntax-e #'name) = ,(syntax-e #'expr))]))

(foo-p #'(a = 1))

This one also work as expected, and even worked for external one, but this cannot apply in my case. Because another form for building inductive type, I need data keyword, let's change program a little bit.

(define (foo-p stx)
  (syntax-parse stx
    #:literals (= data)
    [(name = expr)
     `(,(syntax-e #'name) = ,(syntax-e #'expr))]))

This time, it failed with the following message:

syntax-parse: literal is unbound in phase 0 (phase 0 relative to the enclosing module) in: data

Annoying, but we can fix it in simple way, by change #:literals to #:datum-literals.

(define (foo-p stx)
  (syntax-parse stx
    #:datum-literals (= data)
    [(name = expr)
     `(,(syntax-e #'name) = ,(syntax-e #'expr))]))

Unfortunately, this one also not the best solution. The best one provided by shhyou, creates a dummy syntax and using #:literals.

(provide data)

(define-syntax (data stx) (raise-syntax-error 'dummy))

(define (foo-p stx)
  (syntax-parse stx
    #:literals (= data)
    [(data name)
     `(data ,(syntax-e #'name))]
    [(name = expr)
     `(,(syntax-e #'name) = ,(syntax-e #'expr))]))

In this situation, (foo-p #'(data Nat)) can point to dummy syntax definition part! This would be really helpful to help users of module language realize where the form from! Though this solution would be quite complicated once we break the program down into several files so I didn't take it for now. Thanks for reading such a long-long and boring post XD, have a nice day, and hope you get some ideas for next time you provide a module language in Racket.

Date: 2021-01-07 Thu 00:00
Author: Lîm Tsú-thuàn