0
votes

As an exercise in learning the Racket macro system, I've been implementing a unit testing framework, based on the C++ catch framework. One of the features of that framework is that if I write a check like this:

CHECK(x == y); // (check x y)

When the check is violated the error message will print out the values of x and y, even though the macro used is completely generic, unlike other test frameworks that require you to use macros like CHECK_EQUALS, CHECK_GREATER, etc. This is possible through some hackery involving expression templates and operator overloading.

It occurs to me that in Racket you should be able to do an even better job. In the C++ version the macro can't see inside subexpressions, so if you write something like:

CHECK(f(x, g(y)) == z); // (check (= (f x (g y)) z))

When the check is violated you only find out the values of the left and right hand side of the equal sign, and not the values of x, y, or g(y). In racket I expect it should be possible to recurse into subexpressions and print a tree showing each step of the evaluation.

Problem is I have no idea what the best way to do this is:

  • I've gotten fairly familiar with syntax-parse, but this seems beyond its abilities.
  • I read about customizing #%app which almost seems like what I want, but if for example f is a macro, I don't want to print out every evaluation of the expressions that are in the expansion, just the evaluations of the expressions that were visible when the user invoked the check macro. Also not sure if I can use it without defining a language.
  • I could use syntax-parameterize to hijack the meaning of the basic operators but that won't help with function calls like g(y).
  • I could use syntax->datum and manually walk the AST, calling eval on subexpressions myself. This seems tricky.
  • The trace library almost looks like what it does what I want, but you have to give it a list of functions upfront, and it doesn't appear to give you any control over where the output goes (I only want to print anything if the check fails, not if it succeeds, so I need to save the intermediate values to the side as execution proceeds).

What would be the best or at least idiomatic way to implement this?

2
would whoever voted to close mind explaining why? the question is pretty specific, I'm not sure how it could be interpreted as "broad"Joseph Garvin

2 Answers

2
votes

Here is something to get you started.

#lang racket

(require (for-syntax syntax/parse racket/list))

(begin-for-syntax
  (define (expression->subexpressions stx)
    (define expansion (local-expand stx 'expression '()))
    (syntax-parse expansion
      #:datum-literals (#%app quote)
      [x:id      (list #'x)]
      [b:boolean (list #'b)]
      [n:number  (list #'n)]
      ; insert other atoms here
      [(quote literal) (list #'literal)]
      [(#%app e ...)
       (cons stx
             (append-map expression->subexpressions (syntax->list #'(e ...))))]
      ; other forms in fully expanded syntax goes here
      [else
       (raise-syntax-error 'expression->subexpressions
                           "implement this construct"
                           stx)])))

(define-syntax (echo-and-eval stx)
  (syntax-parse stx
    [(_ expr)
     #'(begin
         (display "] ") (displayln (syntax->datum #'expr))
         (displayln expr))]))

(define-syntax (echo-and-eval-subexpressions stx)
  (syntax-parse stx
    [(_ expr)
     (define subs (expression->subexpressions #'expr))
     (with-syntax ([(sub ...) subs])
       #'(begin
           ; sub expressions
           (echo-and-eval sub)
           ...
           ; original expression
           (echo-and-eval expr)))]))


(echo-and-eval-subexpressions (+ 1 2 (* 4 5)))

The output:

] (+ 1 2 (* 4 5))
23
] +
#<procedure:+>
] 1
1
] 2
2
] (#%app * '4 '5)
20
] *
#<procedure:*>
] 4
4
] 5
5
] (+ 1 2 (* 4 5))
23
2
votes

An alternative to printing everything is to add a marker for stuff that should be shown. Here's a rough simple sketch:

#lang racket

(require racket/stxparam)

(define-syntax-parameter ?
  (λ(stx) (raise-syntax-error '? "can only be used in a `test' context")))

(define-syntax-rule (test expr)
  (let ([log '()])
    (define (log! stuff) (set! log (cons stuff log)))
    (syntax-parameterize ([? (syntax-rules ()
                               [(_ E) (let ([r E]) (log! `(E => ,r)) r)])])
      (unless expr
        (printf "Test failure: ~s\n" 'expr)
        (for ([l (in-list (reverse log))])
          (for-each display
                    `("  " ,@(add-between (map ~s l) " ") "\n")))))))

(define x 11)
(define y 22)
(test (equal? (? (* (? x) 2)) (? y)))
(test (equal? (? (* (? x) 3)) (? y)))

which results in this output:

Test failure: (equal? (? (* (? x) 3)) (? y))
  x => 11
  (* (? x) 3) => 33
  y => 22