Wiki

Clone wiki

docs / Debugging C0 Programs

This page collects some hints and advice on debugging C0 programs.

One important piece of advice you won't want to miss is regarding the buffering of print statements: output with print and related functions is not immediately displayed, but buffered until the output of a newline character (\n), at which point the whole accumulated line of is shown. So debugging statements such as print("I got here!"); may never appear if the program aborts subsequently; you should write print("I got here!\n"); instead.

There are several types of errors that can arise in C0 programs. First, there are the static errors which arise before your program is ever executed.

  • Lexical errors, like 0a4 which is not a legal number of identifier.
  • Syntax errors, like f(,3) which is not a legal function call.
  • Type errors, like true != 3 which attempts to compare a boolean and an integer.
  • Static semantics errors, like using a variable before it is initialized.

Next are the dynamic errors which arise during execution.

  • Runtime errors, like 1 / 0 which raises an exception.
  • Contract errors, like f(-1) if f requires its argument to be positive.
  • Logical errors, where the program does not abort but does not give the right answer.

No matter what you suspect your bug is, you should always use the -d flag to enable dynamic checking unless it makes the program too slow to exhibit the problem, or if you are trying to find a performance bug.

On this page we have some hints regarding

  • Syntax errors
  • Debugging with contracts
  • Debugging with print statements
  • Performance debugging

Syntax Errors

  • Unexpected end of file or eof. If you get an unexpected "end of file" error, it probably means you have an opening delimiter {, (, or [ without a corresponding closing delimiter }, ), or ]. A useful way to debug this is to just type a closing ) at the end of the file. Emacs will highlight the corresponding opening delimiter, which must therefore have been unmatched so far. From there it is usually easy to track down the problem. Even better is to let Emacs help you by highlighting matching delimiters as you type, and also indent for you (see next hint).

  • Use <tab> in Emacs. If you are editing your files in Emacs (which is recommended), hitting the <tab> key while on any line while editing C0 code will properly indent this line. If this is not where you expect, then you likely have some kind of an error. For example,

while (i < n)
  printint(i); print("\n");
  i++;

is buggy, because printint(i); is the only statement inside the loop. We forgot to enclose the body in { }. If we had hit the <tab> key will on the line with i++; it would have indented as

while (i < n)
  printint(i); print("\n");
i++;  /* after hitting <tab> on this line */

visually alerting us to that fact. Once we insert the braces, hitting <tab> will reindent to the correct column.

while (i < n) {
  printint(i); print("\n");
  i++;  /* after hitting <tab> on this line again */
}

Runtime Exceptions

Once a program passes all checks for syntax errors and type errors it will be executed. During execution, runtime exceptions can be raised which print an error message and force the execution to halt. In C0, there are arithmetic exceptions, memory exceptions, contract exceptions and library exceptions. Your contracts should always be such that you never encounter an arithmetic or memory exception. Programs that satisfying this are called safe.

Arithmetic Exceptions

These are caused either by a division by zero or by dividing the minimal integer by -1. For historical reasons, an arithmetic exception is displayed either as division by zero or a Floating exception. Both of these are inaccurate, the latter in particular since no floating point numbers are involved at all. You will also notice that no information as to the source of the error is printed. This is because you should write your contracts to catch such problems elsewhere, as contract failures, in which case some error information will be printed.

Memory Exceptions

Memory exceptions are raised when you try to access an array out of bounds, dereference the null pointer, or allocate a negative amount or more memory than available. Again, for historical reasons, memory exceptions are often display as Segmentation fault. Not much information is usually given; instead, your contracts should fail which gives you a better opportunity to debug your program.

There is one other situation that may lead to memory exceptions, namely if you exhaust the stack space allocated for your program. This can happen if you have nonterminating recursive program, so you should look out for missing base cases or possible sources of nonterminating in your recursive programs.

Contract Exceptions

Failing contracts usually print marginally helpful error message with the line of the contract that failed and (in the case of function preconditions) where it was called from (see Debugging with Contracts below). Of course, you need to use the -d flag to make sure appropriate contract exceptions are raised, since otherwise contracts are not checked at all.

Library Exceptions

Some libraries (written in C) may raise exceptions that cannot be handled by C0. In this case you should read the library documentation and examine and guard your library calls.

Debugging with Contracts

First we have to agree that runtime errors, like 1 / 0 should never arise in a correct program. This is absolutely central. Expressions which raise runtime errors in C0 will have undefined behavior in C and therefore have to be avoided at all cost. It is expressions with undefined behavior that are exploited by viruses and other malware that may infect your computer.

Contract errors are a bit different. While they also abort the program, they are sometimes required! For example, a correct implementation of sort(int[] A, int n); (sort the array segment A[0..n)) should signal a contract violation if it is called with a negative n.

Recall that to check contracts, you must compile code with the -d flag. Also remember that contracts are only checked when they are reached during program execution, so good coverage in test cases is still important with contracts.

The types of runtime errors in C0 are:

  • Division by 0, as in 1/0. This prints Floating exception, even though no floating point numbers are involved. You should find divisions a/b and reason whether b might be 0. Use functions' pre- and post-conditions, loop invariants, and other assertions you already have. If you are not sure add a contract //@assert b != 0; somewhere before the division. Now the error message will report a file and line number of the assertion that failed (for example, assert-div0.c0:3.3-3.16: assert failed) and Abort to indicate a failure of contracts.
  • Array reference out of bounds, as in A[-1]. This prints Out of bounds array access and Segmentation fault. You should find array accesses A[e] and reason how they might be out of bounds. If you are not sure, guard the access with a contract //@assert 0 <= e && e < \length(A);. Now the error message will report a file and line number of the assertion that failed.
  • Null pointer dereference, as in **alloc(int*); This prints Attempt to dereference null pointer and Segmentation fault. You should find pointer dereferences (either p->f or *p) and reason whether p might be null. If you are not sure add a contract //@assert p != NULL;. Now the error message will report a file and line number of the assertion that failed.

In each case we can add assertions (or other contracts) to close in on the location and cause of the runtime error.

Debugging with Print Statements

Sometimes it is helpful to see the values of variables, or verify that certain places in the code have been reached. For this, you should include #use <conio> at the beginning of the file and then add print statements in strategic places in your function. An alternative to print statements is to add one ore more assert(false); statements, which are guaranteed to abort the program when reached and will print the line of the statement state aborted.

But beware! Output in C0 (like C and some other languages) may be buffered, which means that the output to the console does not take place immediately, but only when a line of output is complete. So you should always add newlines (as in print("Got here!\n");) or print newlines by themselves (as in print("i = "); printint(i); print("\n");).

If you forget the newline character, \n in strings, then the string may never show and you may be misled as to whether certain places in the code are reached!

Using Backtraces

When debugging code, it's often helpful to know not only what line caused an error, but also the lines of code that led to this failure. Backtracing involves tracing through the series of function calls that lead to an error in the code. This is often most helpful in determining what lines caused a failing test case, a contract failure, segmentation fault, etc., especially when debugging larger codebases that have numerous functions that call upon each other.

If running an executable results in a contract failure, the line of the failing contract is printed out in addition to the backtrace that led to the failure. For example, with the following code:

int powers_of_two(int x)
//@ensures \result >= 0;
{
    if(x == 0) return 1;
    return 2 * powers_of_two(x - 1);
}

void print_powers_of_two(int x) {
    for(int i = 0; i < x; i++) {
        printf("%d\n", powers_of_two(i));
    }
}

int main() {
    print_powers_of_two(40);
    return 0;
}
A postcondition in powers_of_two will fail when it overflows. This leads to the following error messages:
c0rt: test.c0: 4.4-4.25: @ensures annotation failed
c0rt: in a function called from:
          print_powers_of_two (test.c0: 12.24-12.40)
          main (test.c0: 17.5-17.26)
          (program start)
Aborted (core dumped)
The first line tells us the exact line that triggered the error, and in this case, it tells us that an @ensures annotation failed at line 4 in the file test.c0.

The next lines print out the backtrace, a list of the previous function calls that led up to this point. Here we see that this error happened in a function that was called from print_powers_of_two, which was called from main, and so on.

Performance Bugs

A performance bug is a situation where you have a functionally correct program (delivers the right answer), but it is too slow to finish on some sample inputs. An unintended infinite loop is an extreme example of a performance bug. If you do not have a clear idea how long something is expected to take, performance bugs can be hard to identify and harder to fix. If you are lucky, it could just be a problem in the way you are testing your code! Here is a short checklist:

  • Use the cc0 compiler, not coin. Coin is an interpreter and as such too slow in many cases when the input becomes large.
  • Omit the -d flag. Dynamic checking of contracts can change the asymptotic complexity of functions. Do not be deterred by this, however: write expressive contracts (even if they are slow) and test your code on small examples, then run it without dynamic checking on large ones.
  • Remove any computations you might have introduced for the purpose of debugging. For example, if you decided to compute and then print the length of a queue each time around the loop to make sure it is as expected, then you may need to comment out the print statement and the computation of the length to avoid unnecessary computation.
  • Clarify, in comments, the expected computational complexity of the critical functions in your program. For example, a linear search through an unsorted array should be O(n). Then test the functions separately multiple times and verify that the actual runtime is consistent with your expectation.

Updated