-
Notifications
You must be signed in to change notification settings - Fork 0
Debugging C0 Programs
This page collects some hints and advice on debugging C0 programs. Soon, some examples will be added here; for now there are just some brief words of advice.
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
0a4which is not a legal number of identifier. - Syntax errors, like
f(,3)which is not a legal function call. - Type errors, like
true != 3which 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 / 0which raises an exception. - Contract errors, like
f(-1)if f requires it 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.
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 printsFloating 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 to check. 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) andAbortto indicate a failure of contracts. - Array reference out of bounds, as in
A[-1]. This printsOut of bounds array accessandSegmentation fault. You should find array access A[e] and reason how it 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 printattempt to dereference null pointerandSegmentation fault. You should find pointer dereferences (eitherp->for*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 course of the runtime error.
## Debugging with Print StatementsSometimes 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!
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
cc0compiler, not coin. Coin is an interpreter and as such too slow in many cases when the input becomes large. - Omit the
-dflag. 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.