Making reader extensions hygienic
extend-reader
hygienic-app
1 Example:   a hygienic version of quote
8.12

Making reader extensions hygienic🔗ℹ

source code: https://github.com/AlexKnauth/hygienic-reader-extension

 (require hygienic-reader-extension/extend-reader)
  package: hygienic-reader-extension

procedure

(extend-reader reader-proc    
  extend-readtable)  (-> A ... any/c)
  reader-proc : (-> A ... any/c)
  extend-readtable : (-> readtable? #:outer-scope (-> syntax? syntax?) readtable?)
Extends the given reader-proc by making a function that calls it with the current-readtable parameter extended with the result of calling extend-readtable function.

In addition to a readtable, it passes an outer-scope argument to the extend-readtable function, which that function can pass into the readtable procedures it adds. Those readtable procedures should use the hygienic-app function to transform any input syntax objects into the output syntax object.

procedure

(hygienic-app proc    
  stx    
  #:outer-scope outer-scope)  syntax?
  proc : (-> syntax? syntax?)
  stx : syntax?
  outer-scope : (-> syntax? syntax?)
Applies proc to stx, but with extra scopes added to the input and output to make it hygienic.

This is meant to be used within a readtable procedure added by the extend-readtable argument to the extend-reader function. The outer-scope argument should come from the outer-scope argument passed to that extend-readtable function.

1 Example: a hygienic version of quote🔗ℹ

To make a hygienic version of the quote reader macro, the core problem-specific functionality is implemented by this function:

;; add-quote : (-> Syntax Syntax)
(define (add-quote stx)
  #`(quote #,stx))

But to make it into a lang-extension, we need to use make-meta-reader from syntax/module-reader. A basic template for a lang-extension implemented this way is this, in a file with the path of the language directory plus /lang/reader.rkt.

#lang racket
 
(provide (rename-out [-read read] [-read-syntax read-syntax] [-get-info get-info]))
 
(require syntax/module-reader
         lang-extension/meta-reader-util)
 
;; wrap-reader : (-> (-> A ... Any) (-> A ... Any))
(define (wrap-reader reader-proc)
  ....)
 
(define-values [-read -read-syntax -get-info]
  (make-meta-reader
   'hygienic-quote
   "language path"
   lang-reader-module-paths
   wrap-reader ; for read
   wrap-reader ; for read-syntax
   identity))  ; for get-info

To implement wrap-reader, we can use the extend-reader function from hygienic-reader-extension/extend-reader. To use it we need to define an extend-readtable function to pass as the second argument.

(require syntax/module-reader
         lang-extension/meta-reader-util
         hygienic-reader-extension/extend-reader)
 
;; wrap-reader : (-> (-> A ... Any) (-> A ... Any))
(define (wrap-reader reader-proc)
  (extend-reader reader-proc extend-readtable))
 
;; extend-readtable : (-> Readtable #:outer-scope (-> Syntax Syntax) Readtable)
(define (extend-readtable rt #:outer-scope outer-scope)
  (make-readtable rt
    #\' 'terminating-macro quote-proc))
 
;; quote-proc : Readtable-Proc
(define (quote-proc c in src ln col pos)
  ....)

For an unhygienic version, the .... here could be filled in with (add-quote (read-syntax/recursive src in)):

;; add-quote : (-> Syntax Syntax)
(define (add-quote stx)
  #`(quote #,stx))
 
;; extend-readtable : (-> Readtable #:outer-scope (-> Syntax Syntax) Readtable)
(define (extend-readtable rt #:outer-scope outer-scope)
  (make-readtable rt
    #\' 'terminating-macro quote-proc))
 
;; quote-proc : Readtable-Proc
(define (quote-proc c in src ln col pos)
  (add-quote (read-syntax/recursive src in)))

However, to make it hygienic, we need to use the hygienic-app function when applying add-quote to the input.

;; add-quote : (-> Syntax Syntax)
(define (add-quote stx)
  #`(quote #,stx))
 
;; extend-readtable : (-> Readtable #:outer-scope (-> Syntax Syntax) Readtable)
(define (extend-readtable rt #:outer-scope outer-scope)
  (make-readtable rt
    #\' 'terminating-macro quote-proc))
 
;; quote-proc : Readtable-Proc
(define (quote-proc c in src ln col pos)
  (hygienic-app add-quote (read-syntax/recursive src in)
                #:outer-scope ....))

But then what about the #:outer-scope argument? We need to pass in the outer-scope that we got from extend-readtable, somehow, but it needs to go through quote-proc. So instead of quote-proc directly being a Readtable-Proc, we can make it a function that produces one. Then we can have it pass the outer-scope along.

;; add-quote : (-> Syntax Syntax)
(define (add-quote stx)
  #`(quote #,stx))
 
;; extend-readtable : (-> Readtable #:outer-scope (-> Syntax Syntax) Readtable)
(define (extend-readtable rt #:outer-scope outer-scope)
  (make-readtable rt
    #\' 'terminating-macro (quote-proc outer-scope)))
 
;; quote-proc : (-> (-> Syntax Syntax) Readtable-Proc)
(define ((quote-proc outer-scope) c in src ln col pos)
  (hygienic-app add-quote (read-syntax/recursive src in)
                #:outer-scope outer-scope))

And we’re done! The whole file is this:

#lang racket
 
(provide (rename-out [-read read] [-read-syntax read-syntax] [-get-info get-info]))
 
(require syntax/module-reader
         lang-extension/meta-reader-util
         hygienic-reader-extension/extend-reader)
 
;; wrap-reader : (-> (-> A ... Any) (-> A ... Any))
(define (wrap-reader reader-proc)
  (extend-reader reader-proc extend-readtable))
 
(define-values [-read -read-syntax -get-info]
  (make-meta-reader
   'hygienic-quote
   "language path"
   lang-reader-module-paths
   wrap-reader ; for read
   wrap-reader ; for read-syntax
   identity))  ; for get-info
 
;; add-quote : (-> Syntax Syntax)
(define (add-quote stx)
  #`(quote #,stx))
 
;; extend-readtable : (-> Readtable #:outer-scope (-> Syntax Syntax) Readtable)
(define (extend-readtable rt #:outer-scope outer-scope)
  (make-readtable rt
    #\' 'terminating-macro (quote-proc outer-scope)))
 
;; quote-proc : (-> (-> Syntax Syntax) Readtable-Proc)
(define ((quote-proc outer-scope) c in src ln col pos)
  (hygienic-app add-quote (read-syntax/recursive src in)
                #:outer-scope outer-scope))

It’s hygienic because in a file like this:

#lang hygienic-reader-extension/tests/hygienic-quote racket
 
'3                   ; this produces 3, of course
(define (quote x) 5) ; just defining a function, no big deal
(quote 3)            ; this is 5. It's just calling the function
'3                   ; still 3
 
;; this shouldn't conflict with the earlier definition of quote
(define 'foo 6) ; (define (quote foo) 6), sort of
'3              ; and still 3, because that quote does not bind this one
(quote 3)       ; still 5, because this still refers to the function