Building Solid Code - Assertions in EGO

Skip to main content (skip navigation menu)
Letterhead logo






Building Solid Code - Assertions in EGO

 

No programmer likes bugs in his or her code. Still, bugs do not creep into code by themselves. Programmers put bugs into the code, without really wanting to. This "technical note" is about how programmers can avoid some of the practises that result in buggy software.

Most of the text of this paper is general purpose; some of it applies to the EGO development system.

What exactly is a bug?

The introductory paragraph lays all blame with the programmer, and this is not quite fair, to say the least. Apart from bugs in the implementation, there are:

That aside, the focus of this note is to prevent bugs in the implementation of a program.

I also loosely define a bug as "a functioning of the software that goes contrary to the programmer's intentions". If a programmer did not consider supporting Unicode in his product, that is not a bug (it may be a limitation that makes the software unusable for some applications, but it is not a bug). If, however, the software supports Unicode, but translates legacy texts using the wrong "codepage", that is a bug.

Should the compiler/interpreter not catch all bugs?

This issue has both technical and philosophical sides. I will forego all non-technical aspects and only mention that, in practice, there is a trade-off between the "expressiveness" of a computer language and the "enforced correctness" (or "provable correctness") of programs in that language. In extremes, making a programming language in which one cannot make an error, will be so severely restrictive that it cannot do much real work either.

Making a language very "strict" is not a solution if work needs to be done that exceeds the size of a toy program. A too strict language leaves the programmer struggling with the language, whereas a language is supposed to be a simple means to express algorithms in. When they need to work in a strict language, programmers will invent ways around the protection mechanisms:

In conclusion, strict programming languages lead to programmers inventing work-arounds. Sometimes such work-arounds are required, sometimes they are just the easy way out. The most problematic area is that sometimes work-arounds are applied where they are not necessary and occasionally where they do harm.

The EGO language was designed to avoid any artificial verbosity, and its type checking is fairly loose. The goal of the EGO language is to provide the developer with an informal, and convenient to use, mechanism to test whether the program behaves as was intended. This mechanism is called "assertions" and, although the concept of assertions pre-dates the idea of "design by contract", it is most easily explained through the idea of "design by contract".

Design by contract

The "design by contract" paradigm provides an alternative approach for dealing with erroneous conditions. The premise is that the programmer knows the task at hand, the conditions under which the software must operate and the environment.

In such an environment, each subroutine (or macro/co-module) specifies the specific conditions, in the form of assertions, that must hold true before a client may execute the subroutine. In addition, the subroutine may also specify any conditions that hold true after it completes its operation. This is the "contract" of the subroutine.

The name "design by contract" was coined by Bertrand Meyer, the designer of the language Eiffel, and the principles trace back to predicate logic and algorithmic analysis.

For example, a function that computes a square root of a number may specify that its input parameter be non-negative. This is a precondition. It may also specify that its output, when squared, is the input value ±0.01%. This is a postcondition; it verifies that the routine operated correctly. A convenient way to calculate a square root is via Newton iteration. At each iteration, this algorithm gives at least one extra bit (binary digit) of accuracy. This is an invariant (it might be an invariant that is hard to check, though).

Preconditions, postconditions and invariants are similar in the sense that they all consist of a test and that a failed test indicates an error in the implementation. As a result, you can implement preconditions, postconditions and invariants with a single construct: the "assertion". In EGO, the assertion command is called ".verify()" or, in French, ".verifie()".

An assertion is a simple statement that contains a test. If the test outcome is "true", nothing happens. If the outcome is "false", the assert instruction pops up a message box and drops into the debugger.

One additional nicety of assertions is that when you build the retail version of your software ("DIDAX"), the assertions are silenced. In other words, adding many assertions to your code does not slow down the retail version of the software.

Using assertions

Whenever you track down a non-trivial error in your code, think...

Think how this error could occur and, before fixing the error, think what assertion would have trapped this error. Before fixing the error, put that assertion in the code and test that it really does work. Then fix it (and test again).

You have just fixed the error. So it will not happen again. So why leave the assertion in? What is it still good for? There are two goodies:

  1. Bugs sometimes come back, especially if you are working in a team. It is not uncommon that a team member overwrites corrected modules with his or her local modules (I have seen this happen even with "version control" in place). Also, team members may "fix" your fix to the error, as surprising as this sounds.
  2. Most bugs are caused by special circumstances: inputs that were not expected, synchronization errors due to an unforeseen heavy load of the network,... The exceptional conditions that caused this bug may also uncover other bugs.

As already stated, DIDAX removes assertions. So you should never put production code inside an assertion. Similarly, error conditions that can occur on the client's machine (such as a missing file) should be tested for (and handled) explicitly and not via an assertion.

Finally, if you are working in a team and you are adding assertions to existing code, inform people (your team members, your manager) what you are doing. As confident as you may be in yourself and in your colleagues, expect the error rate to go up when you first start using assertions in an existing project. The reason is that erroneous conditions frequently go unnoticed by tests, or they are silently corrected or rejected by other parts of the program code.

Projects are often managed on strict schedules and the status of a software project is often evaluated on the progress of features that are implemented and the amount of bugs that are fixed. If, due to adding assertions, the number of known bugs increases (not because there are more bugs, but because more existing bugs become "known"), some people may get nervous.

The upshot is that, due to there being less "hidden" bugs, the status of the project is clearer and the remaining work is easier to schedule. Assertions also help in gaining confidence in your code. Being able to say that a certain class of bugs can no longer go undetected in your code is nearly as good as saying that none of your team will ever make a bug of that class.