10. Debugging and the Inspector

Amulet contains many features to aide in debugging programs. Many of these are available interactively through the ``Inspector''. Other features are available programmatically. This chapter provides a reference manual for the Inspector and the other debugging facilities, and concludes with a set of recommendations about how to debug various situations that we have seen frequently.

10.1 Introduction

We want to make Amulet programs very easy to develop and debug. Therefore, we have added extensive error checking to Amulet, as well as a number of interactive and tracing tools. These are designed to work with your regular C++ debugging tools like breakpoints. For example, the Inspector will allow you to break into the debugger when a slot is set, but then you need to use your regular C++ debugger to figure out why the slot was set. The debugging features are all implemented using machine-independent code, except for a single routine that breaks into the debugger. Therefore, we assume you can do stack traces and look at variables in your debugger, rather than needing to do this in our tools.

We would very much like to enhance the debugging capabilities of Amulet. If you think of a facility that would be useful, please let us know. An article about the debugging facilities in Amulet is available from the Amulet web site.

10.2 Include Files

The interactive Inspector is included by default in the Amulet library when you build the debugging option (see the Overview chapter for a discussion of the DEBUG vs. non-debug versions). To use the Inspector, if you have the debugging library, you do not need to do anything special in your application. However, if you want to use any of the debugging features procedurally, you must include the debugger.h file, because it is not included by default when you include amulet.h. The most portable way to include the debugger header file is:

#include <amulet.h>
#include DEBUGGER__H

10.3 Inspector

The Inspector is an interactive program that provides access to a large number of debugging features. At its most basic, it displays the slots of an object in a window. The values of most types of slots can be edited. The properties of the slots and the object can be inspected, as well as dependencies of any formulas in the slots. Traces and breakpoints can be set when slots are accessed or set. The Interactor's tracing mechanisms are also available from the Inspector.

10.3.1 Invoking the Inspector

To pop up the Inspector on an object, you can put the cursor over the object, and hit the F1 key (see Section about how to change the keys, if necessary). The inspector will first print out the window and location in the window of the cursor to the transcript (console) window, and then a new top-level window will appear displaying all the slots of the object and their current values. If no object is under the cursor, then the window itself will be inspected. The Inspector first tries to find a primitve (leaf) object under the cursor, but if that fails, then it presents the front-most group object. If the correct object does not appear, it is usually best to get to the object by going up and down the owner/part tree, as will be explained below.

If you press the F2 key over an object, the Inspector will do the same search for an object, but will only print out the position in the window and the object at that position. This is also very useful for finding out the coordinates of a point in the window.

The F3 key will ask in the console window for the name of an object to inspect. The name typed must be the exact name of the object, which is sometimes an easy-to-remember constant name (like Amulet's or the user's prototypes) or often cut and pasted from the output of one of the tracing functions.

The inspector can also be invoked procedurally using either of the functions:

// inspect the specific object
void Am_Inspect(Am_Object object);

// The next one takes the name of the object.  This is useful from an interpreter.
void Am_Inspect(const char * name);
Most of the time when Amulet gets an Am_Error, you are asked if you want to go into the Inspector before going into the debugger (if the library was compiled with DEBUG on). You can type ``y'' or ``n''. If you type ``n'', then you will break right into the C++ debugger so you can look at the stack, etc. If you type ``y'', the system will try to invoke the inspector viewing an appropriate object, and sometimes selects the slot of that object where the problem is. The question is displayed to the console window after the message from Am_Error. For example:

** Set called on slot which is not there.  Use Add instead  Object = 
<Am_Window_1598>, Slot = <MAIN_INSPECTOR_WINDOW[30030]>.
**  Do you want to enter the Inspector viewing object <Am_Window_1598>? (type 
\Qy' or \Qn'): y
Answer was \Qy'.  Starting Inspector...
~~~ Inspecting object <Am_Window_1598>
Since the Amulet system has crashed, it is not always successful at invoking the Inspector, usually because some internal Amulet state has been so messed up by the error that it cannot recover sufficiently to invoke the Inspector. (If you can reproduce one of these situations, please send it to us so we can try to fix it!).

The function that Amulet uses to invoke the inspector from the Am_Error must invoke the Am_Main_Event_Loop so the inspector will operate. It uses the function:

extern void Am_Inspect_And_Main_Loop(const Am_Object& object, 
				     Am_Slot_Key slot = Am_NO_SLOT);
You can also try invoking this function explicitly from your C++ debugger, if necessary. Changing the Keys

If you want to use F1, F2, and F3 for your own functions, you can always eliminate all of debugging facilities, including the inspector, by building a non-debug version of the library (see the Overview Chapter).

If you want to bind the editing functions to different keys, you can use the function:

extern void Am_Set_Inspector_Keys(Am_Input_Char show_key,
				  Am_Input_Char show_position_key,
				  Am_Input_Char ask_key);
which is exported from debugger.h. This function takes an Am_Input_Char, but a string will also work. Passing in the null character to any of these, by using Am_Input_Char(), will mean that that function is not available. The first parameter controls the normal popping up of the Inspector (normally bound to F1), the second parameter is for just printing the object under the mouse (normally F2), and the third is to prompt from the keyboard (normally F3). The test program testinter has a test of rebinding the Inspector's keys using this function.

10.3.2 Overview of Inspector User Interface and Menus

The inspector window is shown above. All of the slots of the objects are shown along with their current values. Inherited slots are shown in blue, and local slots are shown in black. You can click on a slot value to show a cursor, and then the value can be edited.

You can double-click on a slot name, an object name, or a constraint name to select it, and then perform other operations on that slot, object or constraint (such as viewing its properties). For the commands which pop-up windows, you can select names in those windows as well by double-clicking on them. The name which is selected by double-clicking is also copied into the cut buffer (clipboard) so it can be pasted into this or other applications.

Also shown in the Inspector window, below the list of slots (in this case, you would need to scroll down using the scroll bar on the left) is (optionally) a list of the parts of the object, and the instances of the object.

Commands available from the menus are (many of these are described in detail below):

10.3.3 Viewing and Editing Slot Values

When you inspect an object, the values of the slots are displayed. You can control which slots are viewed, by hiding or showing the inherited slots and/or the internal slots. You can also control the sorting of the slots (either alphabetical or in the order they appear in the object).

If you single click with the left mouse button over a slot value, a cursor will appear and you can edit the slot's value. The editing keys are the same as for all other text interactors (Section of the Interactors chapter). Currently, the inspector will not let you change the type of the value in the slot. Therefore, it uses the current type of the value to decide how to parse the input value. You can edit primitive values, like integers, floats and strings. Depending on whether your compiler supports bools as a primitive type, they will either print out as true and false or 1 and 0. Floating point values print out just like integers if there is no fraction part. You can use the slot properties pop-up window to find out the exact slot type.

For slots which contain named values, like styles (Am_Red, Am_Line_8), objects (Am_Rectangle_123), constraint names (windows_is_color) and method names (rectangle_draw), you can type in a new name. Amulet remembers the names of all built-in or user-defined objects, methods and formulas. If you create your own styles or wrappers, you can arrange for them to have names registered in the database using the ``registry'' mechanism defined in registry.h. For any wrapper object, you can register its name using the Am_Register_Name procedure, such as:

  Am_Register_Name (my_color_object, "my_color");
Then, the user will be able to type in my_color as the value of a slot.

Values that have defined a Am_Type_Support for the type ID will also be able to print and be edited using the user-level names. For example, Am_Input_Chars and most of the enumerated values in slots will work this way.

Unfortunately, you cannot yet set the value of slots that are Am_Value_Lists, and you cannot set the items of an Am_Value_List.

10.4 Accessing Debugging Functions Procedurally

Sometimes it might be useful to access the debugging functions from a program, instead of interactively from the Inspector. For example, you program might be crashing even before it fully starts up, so you cannot access the inspector. If you program gets past the Am_Initialize(), then you can still trace slot setting and print the values of the slots of objects. Also, some of these procedures might be executed from a debugger such as gdb that supports calling functions.

The functions for invoking the Inspector procedurally have already been listed:

// inspect the specific object
void Am_Inspect(Am_Object object);
void Am_Inspect(const char * name);
You can cause an object to be ``flashed'' so you can see where it is on the screen. If it is not visible, then this functions writes the reason to the specified stream:

void Am_Flash (Am_Object o, ostream &flashout = cout);
The tracing functions provide significantly more features than are available interactively from the inspector. The tracing and breaking function takes an optional object, an optional slot, and an optional value. Whatever ones of these are supplied will control whether to trace or break. Thus, if only the object is supplied, then the trace or break will happen whenever any of the slots of that object are set. If only a value is supplied, then a trace or break will happen whenever any slot of any object is set to that value. If all three parameters are supplied, then a trace or break will happen only when that slot of that object is set to that value.

void Am_Notify_On_Slot_Set (Am_Object object = Am_No_Object,
				Am_Slot_Key key = 0,
				Am_Value value = Am_No_Value);
void Am_Break_On_Slot_Set (Am_Object object = Am_No_Object,
				Am_Slot_Key key = 0,
Am_Value value = Am_No_Value);

The next procedure clears a slot notify or break set with the above procedures:

void Am_Clear_Slot_Notify (Am_Object object = Am_No_Object,
				Am_Slot_Key key = 0,
				Am_Value value = Am_No_Value);

10.5 Functions useful in the C++ Debugger

We have provided a set of functions and methods to be invoked while in the C++ debugger to find out information about values. Most of these work for any type of Amulet object (objects, wrappers, methods, etc.). Some of these might also be useful for interactive editors and other tools that need to describe values to users.

The first set are methods of most Amulet types, so these are invoked as o.xxx:

The following is a function (not a method)

10.6 Hints on Debugging

This section lists some hints of procedures we have found useful for debugging certain situations that we have found occur more than once. If you know anything that should be added to this list, please let us know!

1) I click on an object but nothing happens: If you think that an interactor should be running, but it doesn't, it is usually very helpful to turn on Interactor tracing from the Inspector, and see what interactors are running, and what slots they are setting. Although this often prints out a tremendous amount of information, and significantly slows down execution, it is usually worth it. Typical reasons that Interactor's don't seem to execute include:
1.1) The Interactor was never added as a part to an object which is a part of a window (or a group in a window, recursively).
1.2) The Am_START_WHERE_TEST of the Interactor does not return an object when you expect it to. Debugging this usually involves setting breakpoints in your start where function.
1.3) The Interactor is running, but you can't see the feedback object, because the feedback object was never added to a window or group in a window.
1.4) The Interactor is running, but the object it is setting slots of is not the object you expected, so setting the slots is not having any effect.
2) My Interactor is modifying the group itself instead of the members of the group, or affecting the members of a group, and I want it to affect the group itself: The default Am_START_WHERE_TEST of Interactors tries to be smart about which object to affect, based on the type of the object the Interactor is attached to. You can specifically tell the Interactor which to affect by setting the Am_START_WHERE_TEST slot to one of the built-in functions (such as Am_Inter_In or Am_Inter_In_Part) or to a custom function.
3) The slot has the wrong value: There are various reasons this might happen:
3.1) If the value is supposed to be calculated by a constraint, see if the constraint was accidentally removed (see number 8).
3.2) Check if there are accidentally two or more constraints in the slot. Sometimes the system sets single-constraint-mode to false and installs an important constraint in the slot, which might override the value you set. To get rid of an inherited constraint, you need to delete the slot in the prototype object, or set the slot to single constraint mode and set its value.
3.3) Maybe the code is setting the slot of the wrong object: put a breakpoint in your code to see what object is being set.
3.4) Maybe your code is setting the slot, but then some other code, like an Interactor or command object's DO method, is setting it afterwards to a different value: put a break or trace on the slot using the Inspector to see when it is being set.
4) I want to set a widget to have a certain value, but it isn't working: To set the value of a widget, you have to set the Am_VALUE slot of the widget itself. Although the Am_VALUE slot of the command object usually provides the same value, it does not affect the widget to set this slot in the command object. For button-type widgets, the value should either be NULL, or the label or ID of the particular item to become the value (based on the value of the Am_LABEL slot or Am_ID slot of the command object for that item).
5) I set the value of a button, or click on it, but I can't see the value: By default, buttons do not stay ``indented'' when they are pressed. If you want a button to show the value, you have to set its Am_FINAL_FEEDBACK_WANTED slot to true.
6) My constraint is crashing or calculating the wrong value: Because constraints are normal C++ code, you can used your debugger to set breakboints in the constraint code to see why it is calculating its current value. Also, in the Inspector, you can select the slot the constraint is in and set a trace or break on the slot to see when the slot is changing value.
7) My application crashes at start up: Usually this is caused by constraints calculating incorrect values. Amulet supports ``Uninitialized'' values for constraints, but sometimes this is not sufficient to allow constraints to work. You probably need to put a few v.Valid() tests, or instead of getting a slot value directly into a variable, use an intermediate Am_Value. For example:
i = 3 / (int)obj.Get(SLOT);  //crashes because returns 0 when not initialized
Am_Value v = obj.Get(SLOT);
if (v.Valid()) {
   i = 3 / (int)v;
8) The constraint in my slot disappeared: Remember that constraints go away by default when the slot is set, so if an interactor or your program sets the slot, the constraint will be removed. You can see when the slot value is set by setting a break or trace on slot setting using the Inspector. To make sure the constraint is retained, you can set multi-constraint to be on for the formula. See Section 3.14.4 of the Ore chapter.
9) The constraint is not being re-evaluated when it should be: Usually, this is because there isn't a dependency where there should be. Sometimes a constraint is first evaluated before the object is created, in which case no dependency is created. You can select the constraint in the Inspector and check its dependencies. Also, it might be useful to trace or break when the slot's value changes and when the depended on slot's value changes, to make sure that they are really changing.

Last Modified: 01:52pm EDT, August 13, 1997