[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[plt-scheme] assertions and the learning of macro-fu



mzscheme seems to be lacking one construct that is key to (my style of)
good software engineering: assertions.  (If they are in fact present,
well, I had fun and learned a lot writing the following, so it's not
like it's lost time. :)  I submit the following as useful to include in
code, if not necessarily in mzscheme itself:

;; assert: does roughly what you'd expect: evaluates its argument, and if
;; arg returns non-false value, continues silently.  Otherwise, abort with
;; assorted useful info.  If you are asserting the negation of something,
;; use (assert not (...)) instead of (assert (not (...))).
;; CAVEAT PROGRAMMOR: Currently, (assert (f x ...)) will evaluate x ... twice
;; in the case of assertion failure.  This does not affect program correctness,
;; but if x ... has side effects, error output may be incorrect.  On the other 
;; hand, using a side-effecty function in an assertion is probably a REALLY BAD
;; IDEA anyway, so I'm not too worried.
(define-syntax (assert stx)
  (let ([handle-assert 
          (lambda (src-stx positive stx)
            (let ([src (syntax-source src-stx)]
                  [line (syntax-line src-stx)])
              (with-syntax ([pfx (lambda (form)
                                   (printf "Assertion failed at ~a~a: ~a~s~n"
                                           src
                                           (if line (format ":~a" line) "")
                                           (if positive "" "not ")
                                           form))]
                            [sfx (lambda () (error "Aborting execution"))]
                            [id-or-not (if positive (lambda (x) x) not)]
                            [print-arg (lambda (x y)
                                         (printf "Value of ~s is ~s~n" x y))])
                (syntax-case stx (assert)
                  [(assert (a b ...))
                   (syntax (unless (id-or-not (a b ...))
                             (pfx '(a b ...))
                             (let ([eargs (list b ...)])  ; b is evaled twice
                               (for-each print-arg '(b ...) eargs))
                             (sfx)))]
                  [(assert a)
                   (syntax
                     (unless (id-or-not a) (pfx 'a) (print-arg 'a a) (sfx)))]))))])
    (syntax-case stx (assert not)
      [(assert not x) (handle-assert stx #f (syntax (assert x)))]
      [(assert x)     (handle-assert stx #t (syntax (assert x)))])))

#| Example/test cases: all but the first should fail with useful info
(assert 3)
(assert #f)
(assert not 3)
(assert (zero? 3))
(define foo 4)
(assert (zero? foo))
(assert (< 5 foo))
(assert (not (zero? 0)))
(assert not (zero? 0))
(let ((quux 5)) (assert (zero? quux)))
(let ((quux 0)) (assert not (< quux 4)))
(assert (or #f #f))
(let ((meef zero?)) (assert (meef 3)))
|#

Getting those last four cases to all work at the same time was the
trickiest part of this; the tradeoff I made was that arguments end up
getting evaled twice.  That's because the thing being asserted could
itself be a macro, so we can't use 'apply' to apply the predicate to its
arguments; there seems to be no way to get macros (e.g. or) to work here
with a singly-evaluated set of arguments, at least not without a great
deal more work.  (I could be wrong, obviously. :)

Anyway, here is the output of those test cases:
> (assert 3)
> (assert #f)
Assertion failed at STDIN: #f
Value of #f is #f
Aborting execution
> (assert not 3)
Assertion failed at STDIN: not 3
Value of 3 is 3
Aborting execution
> (assert (zero? 3))
Assertion failed at STDIN: (zero? 3)
Value of 3 is 3
Aborting execution
> (define foo 4)
> (assert (zero? foo))
Assertion failed at STDIN: (zero? foo)
Value of foo is 4
Aborting execution
> (assert (< 5 foo))
Assertion failed at STDIN: (< 5 foo)
Value of 5 is 5
Value of foo is 4
Aborting execution
> (assert (not (zero? 0)))
Assertion failed at STDIN: (not (zero? 0))
Value of (zero? 0) is #t
Aborting execution
> (assert not (zero? 0))
Assertion failed at STDIN: not (zero? 0)
Value of 0 is 0
Aborting execution
> (let ((quux 5)) (assert (zero? quux)))
Assertion failed at STDIN: (zero? quux)
Value of quux is 5
Aborting execution
> (let ((quux 0)) (assert not (< quux 4)))
Assertion failed at STDIN: not (< quux 4)
Value of quux is 0
Value of 4 is 4
Aborting execution
> (assert (or #f #f))
Assertion failed at STDIN: (or #f #f)
Value of #f is #f
Value of #f is #f
Aborting execution
> (let ((meef zero?)) (assert (meef 3)))
Assertion failed at STDIN: (meef 3)
Value of 3 is 3
Aborting execution



Ok, now here's the weird thing that's got me completely stumped.  If you
paste this into a mzscheme session directly, it works fine.  If you load
it into mzscheme, it works fine.  If you put this into a module, and you
require it from the interpreter prompt, then positive assertions (e.g.
(assert 3)) work fine, but negative assertions give a syntax error:

Welcome to MzScheme version 200alpha12, Copyright (c) 1995-2002 PLT
> (require "util.sch")
> (assert #f)
Assertion failed at STDIN: #f
Value of #f is #f
Aborting execution
> (assert not 3)
STDIN::53: assert: bad syntax in: (assert not 3)


*However*, if you require it in another module, it works fine again:

(module temp
        mzscheme
        (provide (all-defined))
(require "util.sch") ; includes assert

  (define (meef x)
    (assert x)
    (assert not x))

) ; end module



Welcome to MzScheme version 200alpha12, Copyright (c) 1995-2002 PLT
> (require "temp2.sch")
> (meef 3)
Assertion failed at /Users/blahedo/274/units/temp2.sch:8: not x
Value of x is 3
Aborting execution


So to recap, this assert macro works great *except* when it is in a
module *and* directly required from the repl, but even then, it works in
one syntactic form but not the other.  So obviously the macro is
available (if I haven't required the module containing assert, then it
just complains that assert is an unbound identifier).  Now, clearly my
macro-fu is not at the level of some of the people on this list, but I'm
totally baffled by this behaviour and would love not just a fix but (a
pointer to) an explanation of why this one specific case doesn't work.

-- 
-=-Don Blaheta-=-=-dpb@cs.brown.edu-=-=-<http://www.cs.brown.edu/~dpb/>-=-
When your hammer is C++, everything begins to look like a thumb.
					--Steve Hoflich, comp.lang.c++