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?)
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?)
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