在球拍中记录评估步骤和中间值的宏?
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 中,我希望它应该可以递归到子表达式中并打印出显示评估的每个步骤的树。
问题是我不知道最好的方法是什么:
- 我已经相当熟悉语法分析了,但这似乎超出了它的能力范围。
- 我阅读了关于自定义 #%app 的内容,这几乎是我想要的,但是如果例如 f 是一个宏,我不想打印出扩展中表达式的每个评估,只是用户调用检查宏时可见的表达式的计算。也不确定我是否可以在不定义语言的情况下使用它。
- 我可以使用语法参数化来劫持基本运算符的含义,但这对像 g(y) 这样的函数调用没有帮助。
- 我可以使用 syntax->datum 并手动遍历 AST,自己对子表达式调用 eval。这似乎很棘手。
- 跟踪库几乎看起来像我想要的那样,但你必须预先给它一个函数列表,而且它似乎无法让你控制输出的去向(我只想要如果检查失败则打印任何内容,如果检查成功则不打印任何内容,因此我需要在执行过程中将中间值保存到一边)。
什么是最好的或至少是惯用的实现方式?
这里有一些可以帮助您入门的东西。
#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
作为学习 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 中,我希望它应该可以递归到子表达式中并打印出显示评估的每个步骤的树。
问题是我不知道最好的方法是什么:
- 我已经相当熟悉语法分析了,但这似乎超出了它的能力范围。
- 我阅读了关于自定义 #%app 的内容,这几乎是我想要的,但是如果例如 f 是一个宏,我不想打印出扩展中表达式的每个评估,只是用户调用检查宏时可见的表达式的计算。也不确定我是否可以在不定义语言的情况下使用它。
- 我可以使用语法参数化来劫持基本运算符的含义,但这对像 g(y) 这样的函数调用没有帮助。
- 我可以使用 syntax->datum 并手动遍历 AST,自己对子表达式调用 eval。这似乎很棘手。
- 跟踪库几乎看起来像我想要的那样,但你必须预先给它一个函数列表,而且它似乎无法让你控制输出的去向(我只想要如果检查失败则打印任何内容,如果检查成功则不打印任何内容,因此我需要在执行过程中将中间值保存到一边)。
什么是最好的或至少是惯用的实现方式?
这里有一些可以帮助您入门的东西。
#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