schemeracket

Racket datum->syntax with unquote splicing


In Racket, I am trying to use s-exp to build a new language. So the macro to build an if statement is:

(define-syntax (buildif stx)
  (syntax-case stx ()
    ;split the body (stx) and split into parts
    [(_ condition (then-expr ...) (else-expr ...))
     ;build the if statement
     #'(if condition
          (begin then-expr ...)
          (begin else-expr ...))]))

The problem I have is that I want to convert the syntax into datums using syntax->datum or syntax->list to modify the syntax. However, I cannot then convert the datum->syntax using the unquote splicing to insert the modified values. I want to apply a macro that takes a list of datums and returns a list of datums such that '((+ x y) (- x y)) gives '((- x y) (+ x y)). I want to use this modified list as the values for the if statement

(define-syntax (rev-if stx) ;modified if
  (syntax-case stx ()
    [(_ condition (then-expr ...) (else-expr ...))
     ;convert the syntaxes to datums to manipulate
     (let ((con (syntax->datum #'condition))
           (newthen (opswap (syntax->list #'(then-expr ...))))
           (newelse (opswap (syntax->list #'(else-expr ...)))))
       ;I want to convert the datums back to syntax to execute
       (let ((composed `(if ,con (begin ,@newthen) (begin ,@newelse))))
         (datum->syntax #f composed)))])) 

If I use datum->syntax I get this error: if: unbound identifier; also, no #%app syntax transformer is bound in: if

If I try using:

(syntax/loc stx ;build the syntax for the if
         (if ,con
             ;,@ unquote splicing to place syntax of datums into syntax
              (begin ,@newthen)
              (begin ,@newelse)))

I get this error: unquote: not in quasiquote in: (unquote con)

What I am trying to do is to execute an if branch in reverse. I have defined += -= methods etc. So opswap takes a list such as '((+= x 1) (-= y 2)) and converts it into '((+= y 2) (-= x 1)). I put both branches in begin so that it will execute if passed just one expression or multiple.

(define-for-syntax (opswap oppslist)
     (begin
       (for/list ([item (reverse oppslist)]) ;for every item in the reverse of the opperation list
         (begin
           (cond
           [(and (pair? item) (eq? (car item) '+=)) (cons '-= (cdr item))]
           [(and (pair? item) (eq? (car item) '-=)) (cons '+= (cdr item))]
           [(and (pair? item) (eq? (car item) '*=)) (cons '/= (cdr item))]
           [(and (pair? item) (eq? (car item) '/=)) (cons '*= (cdr item))]
           [(and (pair? item) (eq? (car item) 'displayln) item)];make an exception for displayln as they are diagnostic or display
           [(and (pair? item) (eq? (car item) 'swap) item)] ;call the swap method (it is it's own inverse
           [else item]))))) ;if not assume that it is a method call

Solution

  • The first argument to datum->syntax tells the syntax expander the lexical context that the returned syntax should have, i.e., which identifiers are bound. The way this is communicated is via an existing syntax object; the first argument's lexical context is copied to the return value.

    Passing #f as the first argument means that no identifiers are in scope, not even if, which is why you get the error "if: unbound identifier".

    One way to solve the problem is to use the lexical context at the site of the macro invocation (k).

    (define-syntax (rev-if stx)
      (syntax-case stx ()
        [(k condition (then-expr ...) (else-expr ...))
         (let ((con (syntax->datum #'condition))
               (newthen (opswap (syntax->list #'(then-expr ...))))
               (newelse (opswap (syntax->list #'(else-expr ...)))))
           (let ((composed `(if ,con (begin ,@newthen) (begin ,@newelse))))
             (datum->syntax #'k composed)))]))
    

    You will generally make life easier for yourself and eliminate potential scoping errors by avoiding syntax->datum, when possible. Instead, use syntax-case and with-syntax to deconstruct syntax, and to reconstruct syntax use

    syntax->list is ok because it doesn't act recursively and returns a list of syntax objects.

    Here's what that might look like:

    #lang racket
    
    (define-syntax-rule (+= var val) (set! var (+ var val)))
    (define-syntax-rule (-= var val) (set! var (- var val)))
    (define-syntax-rule (*= var val) (set! var (* var val)))
    (define-syntax-rule (/= var val) (set! var (/ var val)))
    
    (define-for-syntax (opswap exprs)
      (for/list ([expr (reverse (syntax->list exprs))])
        (syntax-case expr ()
          [(op var val)
           (and (identifier? #'var) (eq? (syntax->datum #'op) '+=))
           #'(-= var val)]
          [(op var val)
           (and (identifier? #'var) (eq? (syntax->datum #'op) '-=))
           #'(+= var val)]
          [(op var val)
           (and (identifier? #'var) (eq? (syntax->datum #'op) '*=))
           #'(/= var val)]
          [(op var val)
           (and (identifier? #'var) (eq? (syntax->datum #'op) '/=))
           #'(*= var val)]
          [else expr])))
    
    (define-syntax (rev-if stx)
      (syntax-case stx ()
        [(k condition (then-expr ...) (else-expr ...))
         (with-syntax ([(newthen ...) (opswap #'(then-expr ...))]
                       [(newelse ...) (opswap #'(else-expr ...))])
           #'(if condition
                 (begin (void) newthen ...)
                 (begin (void) newelse ...)))]))
    
    > (define x 1)
    > (rev-if #t ((+= x 1) (/= x 5)) ())
    > x
    4