Debugging Notes 1. Test Cases For test cases, you generally want to test a wide variety of cases. Unfortuantely, complex test cases are generally hard to debug. So, one thing to do after finding a test case that fails, is to simplify the test case as much as possible, while maintaining the failure. Simplification in this case can mean restricting the cases that you code has to deal with. For example, in MP1, simplification might mean dealing with cases where you were only inserting at most 32 bits, or your insertion position was word aligned, or your insertion only affected one word in the target array, or your target array consisted of all 0s. The other thing you can aim for when trying to simplify test cases is to simplify things such that you can mentally (or on paper) figure out exactly what your function will do during its execution. At this point, finding the bug becomes a matter of simply watching the execution of your function and determining when it diverges from your expectations. You can save a lot of time by testing each function independently. If you write a bunch of code and try to debug it all at once, then you have a lot of places the bug can hide. If you exhaustively test each subcomponent as it is developed, then you can focus your testing on a new function and not on any previously tested functions that it calls. 2. Debugger Learn to use the debugger. For xspim, the main things to keep in mind are that you can scroll up and down in the code and data windows by using page up and page down. Also, get comfortable with breakpoints. You'll be using them a lot. The basic idea is, set a breakpoint so you can get as close to the bug as possible, without going past it. After that, step, check that everything seems to be correct, and continue, until you run into the bug. You should have a pretty good idea as to what the correct behavior of your program is, so you can tell when your code diverges from it. If you don't have the bug localized to a particular area, your best bet is to stick breakpoints at strategic areas in the code. Good places are the beginning and ends of functions - make sure that functions are being called with the correct arguments and returning the correct values. If your loops are not iterating too many times, it may be worthwhile to stick a breakpoint at the beginning of major loops - make sure that everything is as you expect it to be before continuing each iteration. If your loops are iterating too many times, you might be better off going for print statements (see the next section). A key idea which is useful in debugging is "divide-and-conquer." In the context of debugging, this means that if you have a test case which produces an incorrect final result, set a breakpoint somewhere (hopefully close to halfway through the execution) and check at that point whether the state of the execution is correct. If it is, then it means the bug is after the breakpoint; if it isn't, then the bug is before the breakpoint. Either way you have reduced your remaining search space by half. Repeat by subdividing the buggy half with another breakpoint. Keep repeating until you find exactly where the bug manifests. Finally, one trick to remember is the if-nop setup. If you've determined that the bug happens, on the 1000th iteration of some loop, you can make setting up a breakpoint simplier, by sticking in a "if (i == 1000) { nop; }" or some assembly equivalent into the beginning of the loop. Then, just add a breakpoint on the no operation in the if statement, and run until you hit the breakpoint. This can save a lot of frustration involved in walking through a loop multiple times. 3. Print Statments If you don't have access to a debugger, or you don't have a easily reproduceable test case, or you have a long stream execution and very little idea of where the bug is, you may want to try to debug via print statement. Basically, modify your program to print out some status information every so often. Two things to watch out for: 1) you are changing the execution of the function - some bugs disappear or be affected by this (mostly timing related bugs) and 2) make sure you do not insert new bugs when you code in the print statement. With assembly, make sure whatever registers you use to print things out do not hold important values. Or at least, do not hold values that you are going to use in the future. The most obvious (and usually fairly useful) place to insert print statments is at the start functions. That way you can trace function calls. You might also want to place print statements at returns. The next obvious place is at the beginning of loops. Most loops should have a set of loop constraints - basically, a set of constraints that you expect to be true at the beginning of each loop. For example, in MP1's insert, in the for loop in the solution, we expect the loop variable to be less than length, we expect that array1 will be identical to the original array1, except with the first i bits of array2 inserted into the appropriate position in array1, and we expect array2 not to be touched. Actually, really, the contents of array1 are probably the most interesting thing there. If we print out the contents of array1 during each loop, we can get a pretty good idea of when array1 goes off course.