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_array() 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
|