在球拍中记录评估步骤和中间值的宏?

Macro to record evaluation steps and intermediate values in Racket?

作为学习 Racket 宏系统的练习,我一直在实现一个基于 C++ catch framework 的单元测试框架。该框架的一个特点是,如果我这样写支票:

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

当检查被违反时,错误消息将打印出 x 和 y 的值,即使使用的宏是完全通用的,这与其他要求您使用宏的测试框架不同 CHECK_EQUALS, CHECK_GREATER,等等。这可以通过一些涉及表达式模板和运算符重载的黑客技术来实现。

我突然想到,在 Racket 中你应该能够做得更好。在 C++ 版本中,宏看不到子表达式内部,所以如果你这样写:

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

当检查被违反时,你只能找出等号左右两边的值,而不是 x、y 或 g(y) 的值。在 racket 中,我希望它应该可以递归到子表达式中并打印出显示评估的每个步骤的树。

问题是我不知道最好的方法是什么:

什么是最好的或至少是惯用的实现方式?

这里有一些可以帮助您入门的东西。

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

输出:

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

打印所有内容的另一种方法是为应该显示的内容添加标记。这是一个粗略的简单草图:

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

导致此输出:

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