The best kittens, technology, and video games blog in the world.

Wednesday, May 09, 2007

Incremental macro building for RLisp

huren...\:D/\:D/\:D/\:D/ by ariffjrs from flickr (CC-NC-SA)RLisp unit testing framework is based on pattern matching. Its core is assert macro which defines a domain specific language for testing:

(require "rlunit.rl")

(test-suite RLUnitExample
(test example
(assert 9 == (+ 2 7))
(assert 2 == (- 6 4))
(assert 2 == (- 6 4) msg: "Subtraction should work")
(assert nil? nil)
(assert 1.99 == 2.02 delta: 0.05 msg: "Deviation is too high")
(assert '() instance_of? Array)
(assert '(foo bar) instance_of? Array)
(assert '(foo bar) kind_of? Enumerable)
(assert () != nil)
(assert #f same: false)
(assert #t same: true)
The framework had just one tiny problem. In Fabien's words:
What you'd need now is a way to dynamically extend your matches with additional patterns, so that you can plug new kinds of tests without hacking the framework's sources.
And that's pretty much what I did. Patterns are kept in hash table pattern-macros-db. pattern-macro-create creates a new macro, pattern-macro-extend adds new patterns (prepended to the list), and pattern-macro-recompile recompiles the macro.
(let pattern-macros-db (hash))

(defmacro pattern-macro-recompile (name)
`(defmacro ,name args
(match args ,@[pattern-macros-db get name])

(defmacro pattern-macro-create (name . patterns)
`[pattern-macros-db set ',name ',patterns]

(defmacro pattern-macro-extend (name . patterns)
`[pattern-macros-db set ',name
[',patterns + [pattern-macros-db get ',name]]
Now the definition of assert macro looks like:
(pattern-macro-create assert
('nil? a) `[self assert_nil ,a]
('nil? a 'msg: msg) `[self assert_nil ,a ,msg]

(a '=~ b) `[self assert_match ,b ,a]
(a '=~ b 'msg: msg) `[self assert_match ,b ,a ,msg]

(a '!~ b) `[self assert_no_match ,b ,a]
(a '!~ b 'msg: msg) `[self assert_no_match ,b ,a ,msg]

(a '!= b) `[self assert_not_equal ,b ,a]
(a '!= b 'msg: msg) `[self assert_not_equal ,b ,a ,msg]

(a 'same: b) `[self assert_same ,b ,a]
(a 'same: b 'msg: msg) `[self assert_same ,b ,a ,msg]

(a 'not_same: b) `[self assert_not_same ,b ,a]
(a 'not_same: b 'msg: msg) `[self assert_not_same ,b ,a ,msg]

(a 'kind_of? b) `[self assert_kind_of ,b ,a]
(a 'kind_of? b 'msg: msg) `[self assert_kind_of ,b ,a ,msg]

(a 'instance_of? b) `[self assert_instance_of ,b ,a]
(a 'instance_of? b msg: msg) `[self assert_instance_of ,b ,a ,msg]

('block: blk) `[self assert_block & (fn () ,@blk)]
('block: blk 'msg: msg) `[self assert_block ,msg & (fn () ,@blk)]
('msg: msg 'block: blk) `[self assert_block ,msg & (fn () ,@blk)]

(a '== b 'delta: c) `[self assert_in_delta ,b ,a ,c]
(a '== b 'delta: c
'msg: msg) `[self assert_in_delta ,b ,a ,c ,msg]
(a '== b 'msg: msg
'delta: c) `[self assert_in_delta ,b ,a ,c ,msg]

(a '== b) `[self assert_equal ,b ,a]
(a '== b 'msg: msg) `[self assert_equal ,b ,a ,msg]

(a 'macroexpands: b) `(assert (macroexpand-rec ',a) == ',b)
(a 'macroexpands: b
'msg: msg) `(assert (macroexpand-rec ',a) == ',b msg: ,msg)

(a b) `[self assert_equal ,b ,a]
(a b 'msg: msg) `[self assert_equal ,b ,a ,msg]
(a) `[self assert ,a]
(a 'msg: msg) `[self assert ,a ,msg]

(raise SyntaxError [(cons 'assert args) inspect_lisp])
(pattern-macro-recompile assert)

And it can be extended trivially, also allowing recursive definitions:
(pattern-macro-extend assert
(rlisp 'syntax: ruby) `(assert ,rlisp == (ruby-eval ,ruby))
(rlisp 'not_syntax: ruby) `(assert ,rlisp != (ruby-eval ,ruby))
(pattern-macro-recompile assert)

(test-suite Syntax
(test true_class
(assert true syntax: "true")
(assert #t syntax: "true")
(assert 't not_syntax: "true")
(test false_class
(assert false syntax: "false")
(assert #f syntax: "false")
(assert nil not_syntax: "false")
(assert () not_syntax: "false")
(test nil_class
(assert nil syntax: "nil")
(assert () not_syntax: "nil")
; ...
Explicit calls to (pattern-macro-recompile ...) are required.
RLisp processes files one global statement at time. So (a) (b) is processed:
  • Macroexpand (a)
  • Execute (a)
  • Macroexpand (b)
  • Execute (b)
On the other hand (do (a) (b)) is processed:
  • Macroexpand (a)
  • Macroexpand (b)
  • Execute (a)
  • Execute (b)
It only makes a difference when execution of (a) affects macroexpansion of (b). What would be the case if one macro tried to expand to (do (update pattern-macros-db ...) (pattern-macros-recompile ...)) - the recompilation would be macroexpanded before pattern-macros-db would get updated. eval would of course work, as would some other tricks, but I think explicit recompilation is not that bad.

1 comment:

murphee said...

Nice; have you seen this paper on Active Patterns in F#:

They also allow to extend their pattern matching capabilities.