Arc Forumnew | comments | leaders | submitlogin
Requesting a code review: delaying macroexpansion with eval
3 points by zck 3836 days ago | 3 comments
So I've long wanted my unit test library (on the off chance someone hasn't seen it yet: https://bitbucket.org/zck/unit-test.arc) to delay macroexpansion until the unit test is actually ran. This is useful for several reasons: mainly that you can change the definition of a macro and be able to simply re-run the test, but also so that you can write tests before you define the macros they use.

Fallintothis was good enough to spend a ton of time teaching me about some intricacies of macros (https://bitbucket.org/zck/unit-test.arc/issue/10/if-a-macro-is-redefined-any-existing-tests), and I came away thinking it would be basically impossible to delay macroexpansion time.

Then I came across lisp-unit: http://www.cliki.net/lisp-unit . Dang, it does exactly what I want.

How? It stores the body of a test as a list of forms, then passes that list to run-code:

    (defun run-code (code)
      "Run the code to test the assertions."
      (funcall (coerce `(lambda () ,@code) 'function)))
Code from https://github.com/OdonataResearchLLC/lisp-unit/blob/master/lisp-unit.lisp#L646

And it works!

    [1]>   (defun run-code (code)
          "Run the code to test the assertions."
          (funcall (coerce `(lambda () ,@code) 'function)))
    RUN-CODE

    [2]> (run-code '((test-macro)))

    *** - EVAL: undefined function TEST-MACRO
    The following restarts are available:
    USE-VALUE      :R1      Input a value to be used instead of (FDEFINITION 'TEST-MACRO).
    RETRY          :R2      Retry
    STORE-VALUE    :R3      Input a new value for (FDEFINITION 'TEST-MACRO).
    ABORT          :R4      Abort main loop
    Break 1 [3]> abort
    [4]> (defmacro test-macro () ''value)
    TEST-MACRO
    [5]> (run-code '((test-macro)))
    VALUE
Wow, pretty cool. Can we do this in arc? We run into a problem coercing a list to a function; it errors. Check out the definition of ar-coerce in ac.scm for why.

    arc> (coerce '(lambda () 3) 'fn)
    Error: "Can't coerce (lambda nil 3 . nil) fn"
Ugh, so that seems to be out. But...what about eval?

    arc> (def run-code (code)
         (eval `(do ,@code)))
    #<procedure: run-code>
    arc> (run-code '((test-macro)))
    Error: "_test-macro: undefined;\n cannot reference undefined identifier"
    arc> (mac test-macro () (prn "macroexpansion time!") ''value)
    #(tagged mac #<procedure: test-macro>)
    arc> (run-code '((test-macro)))
    macroexpansion time!
    value
Well, I'll be damned. This seems to work, and it delays macroexpansion time. It was especially easy to add this to unit-test.arc: https://bitbucket.org/zck/unit-test.arc/commits/8a0d5eb9a3a30600c3739f6d8dbc48e85c91506a

And now macroexpansion time is when we run the tests!

    arc> (suite macroexpansion-test only-test (assert-same 'return (my-test-macro)))
    Successfully created suite macroexpansion-test with 1 test and 0 nested suites.
    #hash()
    arc> (run-suite macroexpansion-test)

    Running suite macroexpansion-test
    macroexpansion-test.only-test failed: _my-test-macro: undefined;
     cannot reference undefined identifier
    In suite macroexpansion-test, 0 of 1 tests passed.

    Oh dear, 1 of 1 failed.
    nil
    arc> (mac my-test-macro () (prn "macroexpansion time!") ''return)
    #(tagged mac #<procedure: my-test-macro>)
    arc> (run-suite macroexpansion-test)

    Running suite macroexpansion-test
    macroexpansion time!
    In suite macroexpansion-test, all 1 tests passed!
    Yay! The single test passed!
    nil
Are there any downsides to this? Because of the way setup is done, setup also works fine (code elided; this is gonna be long enough without it). I'm somewhat wary to put in an eval, but I don't quite see the harm here. I know it's somewhat of a code smell, but none of the reasons that eval is bad seem to apply here^1 . I'd love any feedback you have to offer, or to point out any problems I'll run into.

[1] To present an honest attempt to understand the reasons eval is bad, and why I think they don't apply:

1. Eval is slow. To the small amounts of code that will be eval'd, this seems like premature optimization. This is especially true since I don't see another way of getting this feature.

2. Eval'd code can't refer to variables lexically bound. Tests should not be written this way. If you need to refer to variables outside the body of the test you write, suite-w/setup should be used. And that works fine.

3. It makes code confusing. Again, there's no other way of doing this, to my knowledge. And it's not much more confusing than the code already was.



3 points by fallintothis 3834 days ago | link

It's not often the case, but when eval is right, it's right.

Good work! Looks like you have all the angles covered in your point-by-point breakdown at the end, and evaluating is really all the CL version is doing too. Honestly, the biggest "gotcha" for me is point 2, but you neatly deal with that. The tradeoff is totally worth it for having on-line "redefinition" of the macros you're testing. Just make sure 2 is documented as a caveat, and you're good to go.

Certainly a shorter answer to the whole discussion than the naysaying I launched into. ;)

-----

4 points by rocketnia 3836 days ago | link

Anything that macroexpands and then executes code is going to be an awful lot like 'eval, so this looks like a good approach to me.

-----

1 point by akkartik 3836 days ago | link

Most promising! I'll need some time to play with this.

-----