Assertions

Richard Suchenwirth 2004-11-18 - Checking conditions is a frequent operation in coding. Absolutely intolerable conditions can just throw an error:

   if {$temperature > 100} {error "ouch... too hot!"}

Where the error occurred is evident from ::errorInfo, which will look a bit clearer (no mention of the error command) if you code

   if {$temperature > 100} {return -code error "ouch... too hot!"}   

If you don't need hand-crafted error messages, you can factor such checks out to an assert command:

 proc assert condition {
    if {![uplevel 1 expr $condition]} {
        return -code error "assertion failed: $condition"
    }
 }

Use cases look like this:

   assert {$temperature <= 100}

Note that the condition is reverted - as "assert" means roughly "take for granted", the positive case is specified, and the error is raised if it is not satisfied.

Tests for internal conditions (that do not depend on external data) can be used during development, and when the coder is sure they are bullet-proof to always succeed, (s)he can turn them off centrally in one place by defining

 proc assert args {}

This way, assertions are compiled to no bytecode at all, and can remain in the source code as a kind of documentation.

If assertions are tested, it only happens at the position where they stand in the code. Using a trace, it is also possible to specify a condition once, and have it tested whenever a variable's value changes:

 proc assertt {varName condition} {
    uplevel 1 [list trace add variable $varName write "assert $condition ;#"]
 }

The ";#" at the end of the trace causes the additional arguments name element op, that are appended to the command prefix when a trace fires, to be ignored as a comment.

Testing:

 % assertt list {[llength $list]<10}
 % set list {1 2 3 4 5 6 7 8}
 1 2 3 4 5 6 7 8
 % lappend list 9 10
 can't set "list": assertion failed: 10<10

The error message isn't as clear as could be, because the [llength $list] is already substituted in it. But I couldn't find an easy solution to that quirk in this breakfast fun project - backslashing the $condition in the assertt code sure didn't help. Better ideas welcome.

In any case, these few lines of code give us a kind of bounds checking - the size of Tcl's data structures is in principle only bounded by the available virtual memory, but runaway loops may be harder to debug, compared to a few assertt calls for suspicious variables:

 assertt aString {[string length $aString]<1024}

or

 assertt anArray {[array size anArray] < 1024*1024}

FPX notes that assert(), in the C and C++ programming languages, is a debugging aid during development. Assertions are usually disabled in production code. When the NDEBUG preprocessor symbol is defined, assertions are not evaluated.

Tcllib's control::assert is similar, in that assertions can be enabled and disabled.

In that light, assertions should only be used for "sanity checks," to validate the developer's logic and algorithms under development. Error conditions that might reasonably occur at runtime, such as the failure to open a file, or the aforementioned example of excessive ambient temperature, should be handled by a regular error.

NEM There is a good quote about assertions that I can't find now. It is something to the effect of "If I use a safety net (assertions) during debug, when nothing depends on the code, why would I then remove this safety net in production code, where the cost of errors are much much greater?" The idea being that asserts are meant to catch critical logic errors in code, and so you definitely do want to catch these in production code as quickly as possible. Removing assertions from production code is a performance optimisation, which should only be done if you are sure you have eliminated all bugs (i.e. never). Unless you are putting asserts into the middle of performance-critical loops, then there is really little performance hit from leaving them enabled.

EPSJ Some assertions can transform O(log(n)) algorithms in O(n) or worse. In those cases removing assertions is production runs is not only an optimisation. This is particularly true when you check a data structure consistency at the beginning and/or end of procedures. Assertions are important to catch bugs in complex data structures, but need to be bypassed after validation.


Sarnold I usually bind assertions with custom errors, this way:

proc assert {cond {msg "assertion failed"}} {
    if {![uplevel 1 expr $cond]} {error $msg}
}

# example
assert {$argv==1} "usage : myscript.tcl filename"

Then I can manage error strings from a error (catch) handler that adds further information for the user. However, it is more of sugar for error-handling and these assertions are not meant to be disabled for production code.


Tcllib has a control::assert with more bells and whistles.

See also