| Intermediate LPC |
| Descartes of Borg |
| November 1993 |
| |
| Chapter 7: Debugging |
| |
| 7.1 Types of Errors |
| By now, you have likely run into errors here, there, and everywhere. In |
| general, there are three sorts of errors you might see: compile time |
| errors, run time errors, and malfunctioning code. On most muds you |
| will find a personal file where your compile time errors are logged. For |
| the most part, this file can be found either in your home directory as the |
| file named "log" or ".log", or somewhere in the directory "/log" as a file |
| with your name.. In addition, muds tend to keep a log of run time errors |
| which occur while the mud is up. Again, this is generally found in |
| "/log". On MudOS muds it is called "debug.log". On other muds it may |
| be called something different like "lpmud.log". Ask your administrators |
| where compile time and run time errors are each logged if you do not |
| already know. |
| |
| Compile time errors are errors which occur when the driver tries to load |
| an object into memory. If, when the driver is trying to load an object |
| into memory, it encounters things which it simply does not understand |
| with respect to what you wrote, it will fail to load it into memory and log |
| why it could not load the object into your personal error log. The most |
| common compile time errors are typos, missing or extra (), {}. [], or "", |
| and failure to declare properly functions and variables used by the |
| object. |
| |
| Run time errors occur when something wrong happens to an object in |
| memory while it is executing a statement. For example, the driver |
| cannot tell whether the statement "x/y" will be valid in all circumstances. |
| In fact, it is a valid LPC expression. Yet, if the value of y is 0, then a |
| run time error will occur since you cannot divide by 0. When the driver |
| runs across an error during the execution of a function, it aborts |
| execution of the function and logs an error to the game's run time error |
| log. It will also show the error to this_player(), if defined, if the player |
| is a creator, or it will show "What?" to players. Most common causes |
| for run time errors are bad values and trying to perform operations with |
| data types for which those operations are not defined. |
| |
| The most insideous type of error, however, is plain malfunctioning |
| code. These errors do not log, since the driver never really realizes that |
| anything is wrong. In short, this error happens when you think the code |
| says one thing, but in fact it says another thing. People too often |
| encounter this bug and automatically insist that it must be a mudlib or |
| driver bug. Everyone makes all types of errors though, and more often |
| than not when code is not functioning the way you should, it will be |
| because you misread it. |
| |
| 7.2 Debugging Compile Time Errors |
| Compile time errors are certainly the most common and simplest bugs to |
| debug. New coders often get frustrated by them due to the obscure |
| nature of some error messages. Nevertheless, once a person becomes |
| used to the error messages generated by their driver, debugging compile |
| time errors becomes utterly routine. |
| |
| In your error log, the driver will tell you the type of error and on which |
| line it finally noticed there was an error. Note that this is not on which |
| line the actual error necessarily exists. The most common compile time |
| error, besides the typo, is the missing or superfluous parentheses, |
| brackets, braces, or quotes. Yet this error is the one that most baffles |
| new coders, since the driver will not notice the missing or extra piece |
| until well after the original. Take for example the following code: |
| |
| 1 int test(string str) { |
| 2 int x; |
| 3 for(x =0; x<10; x++) |
| 4 write(x+"\n"); |
| 5 } |
| 6 write("Done.\n"); |
| 7 } |
| |
| Depending on what you intended, the actual error here is either at line 3 |
| (meaning you are missing a {) or at line 5 (meaing you have an extra }). |
| Nevertheless, the driver will report that it found an error when it gets to |
| line 6. The actual driver message may vary from driver to driver, but no |
| matter which driver, you will see an error on line 6, since the } in line 5 |
| is interpreted as ending the function test(). At line 6, the driver sees that |
| you have a write() sitting outside any function definition, and thus |
| reports an error. Generally, the driver will also go on to report that it |
| found an error at line 7 in the form of an extra }. |
| |
| The secret to debugging these is coding style. Having closing } match |
| up vertically with the clauses they close out helps you see where you are |
| missing them when you are debugging code. Similarly, when using |
| multiple sets of parentheses, space out different groups like this: |
| if( (x=sizeof(who=users()) > ( (y+z)/(a-b) + (-(random(7))) ) ) |
| As you can see, the parentheses for the for() statement, are spaced out |
| from the rest of the statement. In addition, individual sub-groups are |
| spaced so they can easily be sorted out in the event of an error. |
| |
| Once you have a coding style which aids in picking these out, you learn |
| which error messages tend to indicate this sort of error. When |
| debugging this sort of error, you then view a section of code before and |
| after the line in question. In most all cases, you will catch the bug right |
| off. |
| |
| Another common compile time error is where the driver reports an |
| unknown identifier. Generally, typos and failure to declare variables |
| causes this sort of error. Fortunately, the error log will almost always |
| tell you exactly where the error is. So when debugging it, enter the |
| editor and find the line in question. If the problem is with a variable and |
| is not a typo, make sure you declared it properly. On the other hand, if |
| it is a typo, simply fix it! |
| |
| One thing to beware of, however, is that this error will sometimes be |
| reported in conjunction with a missing parentheses, brackets, or braces |
| type error. In these situations, your problem with an unknown identifier |
| is often bogus. The driver misreads the way the {} or whatever are |
| setup, and thus gets variable declarations confused. Therefore make |
| sure all other compile time errors are corrected before bothering with |
| these types of errors. |
| |
| In the same class with the above error, is the general syntax error. The |
| driver generates this error when it simply fails to understand what you |
| said. Again, this is often caused by typos, but can also be caused by not |
| properly understanding the syntax of a certain feature like writing a for() |
| statement: for(x=0, x<10, x++). If you get an error like this which is |
| not a syntax error, try reviewing the syntax of the statement in which the |
| error is occurring. |
| |
| 7.3 Debugging Run Time Errors |
| Run time errors are much more complex than their compile time |
| counterparts. Fortunately these errors do get logged, though many |
| creators do not realise or they do not know where to look. The error log |
| for run time errors are also generally much more detailed than compile |
| time errors, meaning that you can trace the history of the execution train |
| from where it started to where it went wrong. You therefore can setup |
| debugging traps using precompiler statements much easier using these |
| logs. Run time errors, however, tend to result from using more |
| complex codign techniques than beginners tend to use, which means you |
| are left with errors which are generally more complicated than simple |
| compile time errors. |
| |
| Run time errors almost always result from misusing LPC data types. |
| Most commonly, trying to do call others using object variables which are |
| NULL, indexing on mapping, array, or string variables which are |
| NULL, or passing bad arguments to functions. We will look at a real |
| run time error log from Nightmare: |
| |
| Bad argument 1 to explode() |
| program: bin/system/_grep.c, object: bin/system/_grep |
| line 32 |
| ' cmd_hook' in ' std/living.c' (' |
| std/user#4002')line 83 |
| ' cmd_grep' in ' bin/system/_grep.c' (' |
| bin/system/_grep')line 32 |
| Bad argument 2 to message() |
| program: adm/obj/simul_efun.c, object: adm/obj/simul_efun |
| line 34 |
| ' cmd_hook' in ' std/living.c' (' |
| std/user#4957')line 83 |
| ' cmd_look' in ' bin/mortal/_look.c' (' |
| bin/mortal/_look')line 23 |
| ' examine_object' in ' bin/mortal/_look.c' (' |
| bin/mortal/_look')line 78 |
| ' write' in 'adm/obj/simul_efun.c' (' |
| adm/obj/simul_efun')line 34 |
| Bad argument 1 to call_other() |
| program: bin/system/_clone.c, object: bin/system/_clone |
| line 25 |
| ' cmd_hook' in ' std/living.c' (' |
| std/user#3734')line 83 |
| ' cmd_clone' in ' bin/system/_clone.c' (' |
| bin/system/_clone')line 25 |
| Illegal index |
| program: std/monster.c, object: |
| wizards/zaknaifen/spy#7205 line 76 |
| ' heart_beat' in ' std/monster.c' |
| ('wizards/zaknaifen/spy#7205')line |
| 76 |
| |
| All of the errors, except the last one, involve passing a bad argument to a |
| function. The first bug, involves passing a bad first arument to the efun |
| explode(). This efun expects a string as its first argment. In debugging |
| these kinds of errors, we would therefore go to line 32 in |
| /bin/system/_grep.c and check to see what the data type of the first |
| argument being passed in fact is. In this particular case, the value being |
| passed should be a string. |
| |
| If for some reason I has actually passed something else, I would be done |
| debugging at that point and fix it simply by making sure that I was |
| passing a string. This situation is more complex. I now need to trace |
| the actual values contained by the variable being passed to explode, so |
| that I can see what it is the explode() efun sees that it is being passed. |
| |
| The line is question is this: |
| borg[files[i]] = regexp(explode(read_file(files[i]), "\n"), exp); |
| where files is an array for strings, i is an integer, and borg is a mapping. |
| So clearly we need to find out what the value of read_file(files[i]) is. |
| Well, this efun returns a string unless the file in question does not exist, |
| the object in question does not have read access to the file in question, or |
| the file in question is an empty file, in which cases the function will |
| return NULL. Clearly, our problem is that one of these events must |
| have happened. In order to see which, we need to look at files[i]. |
| |
| Examining the code, the files array gets its value through the get_dir() |
| efun. This returns all the files in a directory if the object has read access |
| to the directory. Therefore the problem is neither lack of access or non- |
| existent files. The file which caused this error then must have been an |
| empty file. And, in fact, that is exactly what caused this error. To |
| debug that, we would pass files through the filter() efun and make |
| sure that only files with a file size greater than 0 were allowed into the |
| array. |
| |
| The key to debugging a run time error is therefore knowing exactly what |
| the values of all variables in question are at the exact moment where the |
| bug created. When reading your run time log, be careful to separate the |
| object from the file in which the bug occurred. For example, the |
| indexing error above came about in the object /wizards/zaknaifen/spy, |
| but the error occured while running a function in /std/monster.c, which |
| the object inherited. |
| |
| 7.4 Malfunctioning Code |
| The nastiest problem to deal with is when your code does not behave the |
| way you intended it to behave. The object loads fine, and it produces no |
| run time errors, but things simply do not happen the way they should. |
| Since the driver does not see a problem with this type of code, no logs |
| are produced. You therefore need to go through the code line by line |
| and figure out what is happening. |
| |
| Step 1: Locate the last line of code you knew successfully executed |
| Step 2: Locate the first line of code where you know things are going |
| wrong |
| Step 3: Examine the flow of the code from the known successful point to |
| the first known unsuccessful point. |
| |
| More often than not, these problems occurr when you are using if() |
| statements and not accounting for all possibilities. For example: |
| |
| int cmd(string tmp) { |
| if(stringp(tmp)) return do_a() |
| else if(intp(tmp)) return do_b() |
| return 1; |
| } |
| |
| In this code, we find that it compiles and runs fine. Problem is nothing |
| happens when it is executed. We know for sure that the cmd() function |
| is getting executed, so we can start there. We also know that a value of |
| 1 is in fact being returned, since we do not see "What?" when we enter |
| the command. Immediately, we can see that for some reason the |
| variable tmp has a value other than string or int. As it turns out, we |
| issued the command without parameters, so tmp was NULL and failed |
| all tests. |
| |
| The above example is rather simplistic, bordering on silly. |
| Nevertheless, it gives you an idea of how to examine the flow of the |
| code when debugging malfunctioning code. Other tools are available as |
| well to help in debugging code. The most important tool is the use of |
| the precompiler to debug code. With the code above, we have a clause |
| checking for integers being passed to cmd(). When we type "cmd 10", |
| we are expecting do_b() to execute. We need to see what the value of |
| tmp is before we get into the loop: |
| |
| #define DEBUG |
| int cmd(string tmp) { |
| #ifdef DEBUG |
| write(tmp); |
| #endif |
| if(stringp(tmp)) return do_a(); |
| else if(intp(tmp)) return do_b(); |
| else return 1; |
| } |
| |
| We find out immediately upon issuing the command, that tmp has a |
| value of "10". Looking back at the code, we slap ourselves silly, |
| forgetting that we have to change command arguments to integers using |
| sscanf() before evaluating them as integers. |
| |
| 7.5 Summary |
| The key to debugging any LPC problem is always being aware of what |
| the values of your variables are at any given step in your code. LPC |
| execution reduces on the simplest level to changes in variable values, so |
| bad values are what causes bad things to happen once code has been |
| loaded into memory. If you get errors about bad arguments to |
| functions, more likely than not you are passing a NULL value to a |
| function for that argument. This happens most often with objects, since |
| people will do one of the following: |
| 1) use a value that was set to an object that has since destructed |
| 2) use the return value of this_player() when there is no this_player() |
| 3) use the return value of this_object() just after this_object() was |
| destructed |
| |
| In addition, people will often run into errors involving illegal indexing or |
| indexing on illegal types. Most often, this is because the mapping or |
| array in question was not initialized, and therefore cannot be indexed. |
| The key is to know exactly what the full value of the array or mapping |
| should be at the point in question. In addition, watch for using index |
| numbers larger than the size of given arrays |
| |
| Finally, make use of the precompiler to temporarly throw out code, or |
| introduce code which will show you the values of variables. The |
| precompiler makes it easy to get rid of debugging code quickly once you |
| are done. You can simply remove the DEBUG define when you are |
| done. |
| |
| Copyright (c) George Reese 1993 |