next up previous contents index
Next: Bibliographical References Up: Morphine - User Manual Previous: Setting up your debugging

Subsections

    
Extending the debugging environment

In the previous section, we explained how the Morphine debugging environment can be customized ``on the fly'' by setting parameters, and by modifying or adding debugging commands and primitives. Such customizations can be reused by collecting them in a file and compiling them into the Morphine session. Thus, you can easily set up your personal debugging environment. However, if you only compile a collection of predicates you, and especially other users who might want to benefit from your work, are not supported on using your modifications. For example, your new functionalities cannot be seen from the manual.

This chapter will guide you through the process of extending the environment so that other users can straightforwardly benefit from your extensions. Note that all the existing scenarios are build within the frame described here. You have to declare the objects that you want people to use. These objects will then have a nice interface.

When to make a scenario

Assume you have developed a new debugging strategy, and you have implemented and tested the commands needed to apply it. Now you would like to add them to the Morphine environment, in order to get the same support when using your debugging strategy as you get for the strategies already implemented in Morphine, and also to make it available for other users as well. This is the time when you have to make a new debugging scenario.

When you want to make a new scenario, you have to provide the scenario's source code which consists of the Prolog code implementing the debugging strategy, as well as declarations of those functionalities of your scenario that you want others to use, that is the commands, primitives, procedures, parameters, and types. The implementation and the declarations may be distributed among several source files.

The scenario is made by the scenario handler. This is the part of Morphine which is responsible for the integration of a scenario into the debugging environment. Its task is twofold:

$\bullet$
First, the scenario handler translates the scenario's source files. It reads the declarations of Opium objects contained in the source files, and uses them to generate some code which is collected in the object files of the scenario. The idea is that as much as possible of the code related to the objects is generated automatically. This helps to avoid inconsistencies in the code, to reduce redundant information, and to complete the source code by checkings in order to avoid run-time errors. The declarations themselves are also collected in the object files.

$\bullet$
Secondly, the scenario handler loads the scenario. The source files as well as the objects files related to the scenario are compiled in the debugging session, in order to run the scenario. While the scenario is loaded, some predicate flags for the predicates defined in the scenario are set according to the load options. The parameters related to the scenario are initialized automatically. Furthermore, the declarations of the Opium objects are added to the database. They are used to give help on the objects, thus the help information is always up-to-date and syntactically consistent with the code.

In the following sections, we present everything you need to know when you want to make an Opium scenario. First, we give a detailed description of the declarations of Opium objects, and explain what the scenario handler does for each of them. Then we give some advices which might help you when designing your first scenario. Finally, we explain how a scenario is made, and what you have to care about, and not to care about when making a scenario. As an example, the source code of scenario step_by_step   is given in appendix C.

    
The declaration of Opium objects

The Opium objects which shall be treated by the scenario handler have to be announced to Morphine by declarations. The declarations are added to the Prolog code implementing the scenario. This gives a better chance that the declarations are always up-to-date with respect to the current state of the implementation.

In the following, we present an example declaration for each kind of Opium object. We explain what kind of information the scenario handler gathers from the declarations, and what it is going to do with this information when the scenario is made. This will also tell you what the user has to provide, and what will be automatically done by Morphine.

   
Common parts of the declarations

The declaration of an Opium object looks like

    opium_<object>(
            slot1 : value1,
                  :
            slotn : valuen
                  ).
where each slot contains certain information about the object.

There are some slots which appear in all the Opium declarations, or at least in most of them. These slots are explained in this section. The slots which are special in a particular declaration are explained in the respective section.

3.2cm
name
the name of the Opium object. It gives access to the object, and it is used to find help information about the object in the on-line manual, and in the reference manual;

arg_list
the list of arguments of the Opium object. The names of the arguments are used in help messages and error messages, therefore they should be meaningful. The arguments have to be variables, to ensure that the types can be checked automatically;

arg_type_list
the list of types of the object's arguments. There are four admissible types:
$\bullet$
T, where T is an Opium type,
$\bullet$
B, where B is a built-in predicate of Eclipse,
$\bullet$
is_member(L), where L is a list of values,
$\bullet$
is_subset(L), where L is a list of values.

implementation
the predicate which is implementing the object. The idea of separating name and implementation is to support the user on customizing the object (cf. section 5.4). The scenario handler connects the object's name to its implementation, possibly adding some checkings;

message
the help message giving a short description of the object. This message is displayed by the on-line help, and it is also listed in the reference manual.

   
The Opium scenario

The declaration of an Opium scenario has to be contained in the scenario's basefile, that is in a file called ``<scenario>.op''. The declaration of an Opium scenario looks like:

opium_scenario(
        name         : morphine_kernel,
        files        : [ morphine_kernel,
                         forward_move,
                         current_arg,
                         current_slots,
                         event_attributes,
                         exec_control,
                         coprocess],
        scenarios    : [],
        message      :
"Scenario morphine_kernel contains all the basic mechanisms of Morphine \
which are needed to debug Mercury programs. \n\
"
        ).
The slots in the declaration of a scenario have the following meaning:
name
the name which can be used to get help information about the scenario, and which is also used as the title of the respective section in the reference manual;

files
the (relative) names of the Morphine files related to the scenario. All the files related to a scenario have to be in the same directory, called source directory. They have to have an extension ``.op'' indicating that they contain Morphine code;

scenarios
the names of the scenarios which are required in order to run the actual scenario;

message
the help message giving a short description of the scenario, and of its intended use.

This declaration tells the scenario handler which source files are part of the scenario, that is which files have to be treated when the scenario is made. Furthermore, it says which scenarios have to be made together with the declared scenario if they are not yet present in the module where the scenario is loaded.

When the scenario is loaded, its name is added to the menu of scenarios in the window-based user interface.

   
The Opium command

The commands are the objects for which the scenario handler provides the most support. The declaration of a command looks like:

    opium_command(
            name            : next,
            arg_list        : [N],
            arg_type_list   : [integer],
            abbrev          : n,
            interface       : menu,
            command_type    : opium,
            implementation  : next_Op,
            parameters      : [traced_ports],
            message         :
    'Command which prints the N next trace lines according to the 
     "traced_ports" parameter.'
            ).

The slots in the declaration of a command have the following meaning (for the slots not mentioned here, see section 6.2.1):

3.2cm
abbrev
the command's abbreviation. If there is no abbreviation for the command, this slot has to contain a variable;

interface
says how this command shall appear in the window-based user interface. A command may be placed in a button or in a menu, or it may be hidden (see below);

command_type
there are three possible types of a command: opium, trace, and tool (see below);

parameters
the list of parameters which can be set in order to modify the behavior of this command. The meaning of this slot is only to give help information.

For all the commands, the scenario handler connects the name of the command to its implementation. If the command has arguments, a call to predicate check_arg_type/4 is inserted before the implementation is actually called. This predicate checks the types of the arguments, and allows to correct them on the fly if they are wrong . The arguments of a command have to be variables to ensure that unification always succeeds, so that the call to check_arg_type/4 is always reached in order to give proper error messages. For command next/1 whose declaration is given above, the following predicate is generated by the scenario handler:

    next(N) :-
        check_arg_type([N], ['N'], [integer], NewList),
        Cmd =.. [next_Op | NewList],
        Cmd.
where NewList is the list of checked (and possibly corrected) arguments, and next_Op/1 is the name of the command's implementation.

If an abbreviation for the command is given, it is connected to the name of the command. Thus, the command's abbreviation will be updated automatically if the number of arguments is changed. For example, for command next/1 the following connection is generated:

    n(N) :-
        next(N).

Some actions of the scenario handler depend on the type of the command, where the type may be trace, tool, or opium .

$\bullet$
A command of type trace  is intended to determine and show exactly one trace line. The printing of the trace line is not part of the commands implementation. Instead, a call to predicate print_line/0 is added to this command when the scenario is made. The reason is that together with the command a primitive is automatically declared which does exactly the same as the command, except printing the trace line. This primitive is called <command>_np (no print), and its abbreviation is called <abbrev>_np. This primitive can be used by a debugging program to retrieve the same trace line as it is done by the command, without printing it (see the example for skip-till-condition, section 3.4). In case of command next/0 whose implementation is called next_Op/0, the scenario handler generates the following code:
    next :-          % command
        next_np,
        print_event.

    next_np :-       % primitive
        next_Op.

    n_np :-          % abbreviation
        next_np.
If the command has arguments, the checking of argument types is done in the command, not in the primitive.

$\bullet$
A command of type tool  shall be declared as a tool of Eclipse, that is the name of the module where it is called is automatically passed to its implementation. Therefore, the number of arguments in the implementation has to be one more than the number of arguments given in the argument slot, namely the module has to be added as a last argument. The declaration of the command as a tool is done by the scenario handler. For example, command set_default/1 has to set the value of the parameter known in the current module, therefore it is declared as a command of type tool. Its implementation is called set_default_Op/2. The scenario handler generates the following code:
    :- tool(set_default/1, set_default_body/2).

    set_default_body(Parameter, Module) :-
        check_arg_type([Parameter, Module], ['Parameter', 'Module'], 
                        [is_opium_parameter, is_opium_module], NewList),
        BodyCmd =.. [set_default_Op | NewList],
        BodyCmd.

$\bullet$
If a command is of type opium , only its interface is generated by the scenario handler. The behavior of the command is completely controlled by the implementation, including the command's output. The listing of next/1 gives an example for the code generated by the scenario handler for a command of this type (see above).

The integration of the command into a window-based user interface  can be done according to the entry in the interface slot:

$\bullet$
A command is put on a button if its name or abbreviation is short enough. The maximum number of buttons in a scenario is six. If you have declared more than six commands of type button, the remaining ones will be put in the command menu.

$\bullet$
A command with interface menu is added to the command menu.

$\bullet$
A command with interface hidden does not appear in the window-based user interface. Thus, this type has to be used for commands which make sense in the tty interface only.

Currently, there is no Window User Interface for Morphine.

   
The Opium primitive

A primitive is similar to a command, but it does not provide any user interface facilities, because it is supposed to be used in debugging programs. The declaration of a primitive looks like:

    opium_primitive(
        name            : current_arg_types,
        arg_list        : [ListArgTypes],
        arg_type_list   : [is_list_or_var],
        abbrev          : curr_at,
        implementation  : current_arg_types_Op,
        message         :
"Gets or checks the list of the arguments types of the current procedure. \
Unify non-live arguments with the atom '-'"
        ).

The slots in the declaration of a primitive have the following meaning (for the slots not mentioned here, see section 6.2.1):

3.2cm
abbrev
the primitive's abbreviation. If there is no abbreviation for the primitive, this slot has to contain a variable.

The scenario handler connects the name of the primitive to its implementation. If an abbreviation for the primitive is given, it is connected to the name of the primitive. For example, the following code is generated for primitive current_arg_types/1:

    current_arg_types(Goal) :-         % primitive
        current_arg_types_Op(Goal).

    curr_at(Goal) :-                   % abbreviation 
        current_arg_types(Goal).

When the scenario is loaded, the primitive is added to the menu of primitives in the window-based user interface.

   
The Opium procedure

A procedure is a basic predicate which is used to implement commands and primitives. Its declaration looks like:

    opium_procedure(
            name             : write_arg,
            arg_list         : [Arg],
            implementation   : write_arg_Op,
            parameters       : [term_display, list_display],
            message          :
    'Procedure which prints an argument in the trace line.'
        ).

The slots in the declaration of a procedure have the following meaning (for the slots not mentioned here, see section 6.2.1):

3.2cm
parameters
the list of parameters which can be set in order to modify the behavior of the procedure. The meaning of this slot is only to give help information.

The scenario handler connects the name of the procedure to its implementation, thus the following predicate is generated for procedure write_arg/1:

    write_arg(Arg) :-
        write_arg_Op(Arg).

When the scenario is loaded, the procedure is added to the menu of procedures in the window-based user interface.

   
The Opium parameter

An Opium parameter can be used to modify the behavior of a command, a primitive, or a procedure. The declaration of a parameter looks like:

    opium_parameter(
            name             : arg_undisplay,
            arg_list         : [Pred, ArgNo],
            arg_type_list    : [is_pred, integer],
            parameter_type   : multiple,
            default          : nodefault,
            commands         : [write_arg],
            message          : 
    'Parameter which tells which arguments of which predicates have 
    to be NOT displayed. There must be one "arg_undisplay" clause for 
    each argument which shall not be displayed.' 
            ).

The slots in the declaration of a parameter have the following meaning (for the slots not mentioned here, see section 6.2.1):

3.2cm
parameter_type
there are three possible types of a parameter, with the following meaning:
1.8cm
single
the parameter always has exactly one value ;
multiple
the parameter may have several values, or it may not be set at all ;
c
the parameter is a system parameter which is actually implemented in C.

default
the list of default values for the parameter's arguments. If the parameter's type is multiple, the entry may also be nodefault, saying that this parameter does not have any value when the scenario is loaded;

commands
the list of commands whose execution is influenced by the value of this parameter. The meaning of this slot is only to give help information.

The scenario handler declares the parameter as dynamic predicate, as the values of the parameter will be stored in clauses asserted in Morphine. For example, parameter arg_undisplay/2 is declared in the following way:

    :- dynamic arg_undisplay/2.

If a default value is given, a first clause of the dynamic predicate with this default value will be generated and asserted when the scenario is loaded, in order to initialize the parameter.

When the scenario is loaded, the parameter is added to the menu of parameters in the window-based user interface.

   
The Opium type

A type is a predicate which is used to check the arguments of commands and parameters, and to give help information on the arguments. Its declaration looks like:

    opium_type(
            name            : is_proc,
            implementation  : is_proc_Op,
            message         :
    "Type which succeeds for terms of the form 
    [ProcType+][Module:]ProcName[/Arity][-ModeNum] where terms betwenn square 
    bracquets are optional, ProcType has type is_proc_type_attribute/1,
    Module and ProcName have type is_atom_attribute/1, Arity and ModeNum have
    type is_integer_attribute/1.
    ").

For the meaning of the slots, see section 6.2.1.

The scenario handler connects the name of the type to its implementation. Thus, the following code is generated for type is_pred/1:

    is_pred(X) :-
        is_pred_Op(X).

When the scenario is loaded, the type is added to the menu of types in the window-based user interface.

   
Declaring Opium objects with Emacs

The Opium environment contains a file ``opium-mode.el'' with extensions for the Emacs editor. These extensions are interactive functions which support the user on declaring Opium objects. The names of the functions are

$\bullet$
scenario
$\bullet$
command
$\bullet$
primitive
$\bullet$
procedure
$\bullet$
parameter
$\bullet$
type
$\bullet$
demo
according to the names of the Opium objects. If a function is called, it will prompt for the entries in the declaration, giving some help on the possible values. When the declaration is complete, it is added at the current cursor position.

Some advices on designing a new scenario

Assume you have implemented all the predicates needed to run your debugging strategy. Now you want to make the scenario, in order to get support on using it. First, you have to add the declaration of the scenario to the scenario's basefile. This declaration contains the names of all the files related to the scenario, as well as the names of other scenarios which are needed to run the current one.

Then you have to decide upon the commands, primitives, and procedures:

$\bullet$
A predicate which shall be used at toplevel only is a command.

$\bullet$
A predicate which shall be used at toplevel, but can also be used in a debugging program (possibly in another scenario), is a primitive.

$\bullet$
A predicate used by commands and primitives which you or other users might want to customize, or to use in another scenario, is a procedure.

When you enter the argument types of commands and primitives, you may notice that the set of Opium types is not sufficient. If this is the case, you have to declare (and to define!) a new type which is suitable for the arguments used by your commands and primitives.

If parts of your debugging program are variable with respect to certain items, these items should be declared as parameters. This provides flexibility when the scenario is used.

If you want to encourage other users to try your scenario, you can also add automated demos which illustrate the features of your debugging strategy , and give some ideas of how the strategy is supposed to be used.

When deciding upon the objects, remember that the objects are those predicates related to the scenario which can be easily used by another scenario, thus they implement an interface between the scenarios. This is also reflected by the fact that the module declarations for Opium objects are automatically added when the scenario is loaded (cf. section 6.4). Therefore, you should not add any explicit module declaration for a predicate defined in your scenario. If you think this is needed, it means that this predicate should be declared as an Opium object.

For all the objects, add a help message which describes the functionalities of the object, and explains how it is related to other objects. If you need some further help on how to fill the slots of the Opium declarations, you can also have a look at the implementations of the current Opium scenarios.

   
How to make a scenario

The scenario handler which is used to make a new scenario is started by command make. This command is defined in scenario scenario_handler . Actually, there are four commands called make, with a different number of arguments. We explain the use of the most general one, command make/5 . The semantics of the other commands can be seen from the manual. All of them are calling command make/5. In order to test a scenario, use make/1  in a test module.

In a goal make(Scenario, Module, OptionList, SrcDir, ObjDir), argument Scenario is the name of the scenario, Module is the name of the module where the scenario shall be loaded, OptionList is the list of options telling how the scenario shall be loaded (see below), SrcDir is the full path name of the directory containing all the source files related to the scenario, and ObjDir is the full path name of the directory where the object files, that is the files resulting from the translation of the scenario shall be stored. Note: it is important that the full path name is given in exactly the same way as this is done by Eclipse's built-in getcwd/1, as this is used to determine the path name of the current directory which might be compared to the path names given in the make command. The object directory is created automatically if it does not exist yet.

When command make/5 is called, it reads the scenario's base file ``<scenario>.op'' in order to determine the names of the files related to the scenario from the scenario declaration. Then for each source file it checks whether it has been modified since it has been translated for the last time. If this is the case, and only then, the source file is translated again.

When the scenario handler translates a scenario file, it checks the file for Opium declarations, and collects some information from the declarations (see section 6.2). This information is stored in the object files which are compiled when the scenario is loaded. For each source file, there are actually two object files: a load file  which is compiled when the scenario is loaded active (extension ``.load''), and an autoload file  which is compiled when the scenario is loaded inactive (extension ``.autoload'').

When the object files for all the source files related to the scenario are up-to-date, the scenario is loaded, according to the load options given in argument OptionList. There are three pairs of options: active/inactive  , traceable/untraceable  , local/global  . The options have to be given in this order. They have the following meaning:

$\bullet$
if a scenario is made active, its source files and load files are compiled, and it can be immediately used; if a scenario is made inactive, only the autoload files containing the autoload information are compiled, that is the implementation will actually be loaded only when a command related to this scenario is called;

$\bullet$
if a scenario is made traceable its implementation can be traced by another Opium session; if a scenario is made untraceable, the details of its implementation will be skipped, thus it takes less space, and the execution is faster;

$\bullet$
if a scenario is made local it is only available in the module where it is defined; if a scenario is made global its objects can be used in every module, except in the modules where a local scenario with the same name exists.
The latter two kinds of load options are needed even when the scenario is loaded inactive, because they are reused when the implementation is autoloaded.

   
Remaking an existing scenario

The scenario handler can also be used to remake an existing scenario. For example, you might want to change the load options of a scenario in order to make a local scenario global, or to make a scenario untraceable when it has been tested. Furthermore, you can make a local version of an existing scenario in a new module before modifying it, thus the modifications do not affect the original scenario, and the other scenarios which are using it. The following basic scenarios have to be global as they are used by all the extensions, and they cannot be remade in a module other than morphine_kernel:

$\bullet$
scenario_handler 
$\bullet$
morphine_kernel 
$\bullet$
interface 
$\bullet$
source 
$\bullet$
display 

You also might want to remake a scenario because you want some extensions to be integrated into this scenario, or you want to modify the scenario in a deeper way than the modifications presented in section 5. This case is currently not really supported by Opium. There is the following problem. If you want to modify or extend part of the original source code of a scenario, there are two possibilities. Either you make a private copy, but then you do not benefit from updates of Opium, or you modify the installed copy, but then all Opium users have to use your modifications. Therefore, we advise you to create an additional scenario instead, and to load it in the same module as the scenario you want to extend. The only difference is that in the on-line help, and in the window-based user interface you have to check both scenarios instead of one only.

   
Error messages given by the scenario handler

The scenario handler gives an error message in the following cases:

$\bullet$
if a scenario shall be made global, but it is already defined global in another module;

$\bullet$
if the scenario is not declared in the basefile;

$\bullet$
if a scenario file given in the scenario's declaration does not exist;

$\bullet$
if the declaration of an Opium object is not correct or not complete;

$\bullet$
if the default value of an Opium parameter given in the declaration is not correct.

The error message which is given if an Opium declaration is not correct or not complete currently does not give any hint on the kind of error. If the scenario handler complains about a declaration, the following might be wrong:

$\bullet$
a slot is missing,

$\bullet$
a slot entry does not have the proper type (list, list of atoms, etc.),

$\bullet$
an slot entry does not have an admissible value (command_type, interface, default),

$\bullet$
there is an argument of a command which is not a variable,

$\bullet$
the number of arguments and argument types is inconsistent.

Furthermore, you might get error messages from Eclipse when you put explicit module directives into the source code of your scenario, as these may interfere with the work of the scenario handler.

    
Initialization of scenarios

If there are parameters or any other dynamic predicates in a scenario whose values are changed when the scenario is used, you might want to reinitialize these predicates when the execution of a new goal is started in the traced session. For example, parameter zoom_depth/1  in the Morphine zooming 6 scenario has to be reset to 1 in order to start the examination of the new goal from depth 1.

This can be achieved by adding a call to the command initialization/1  to the source code of the scenario. Its argument is a goal which will be executed every time a new execution is started in the traced session. For example, the zooming scenario contains a goal

        :- initialization(set_default(zoom_depth)).
  which will reset the parameter used by the zooming commands.


next up previous contents index
Next: Bibliographical References Up: Morphine - User Manual Previous: Setting up your debugging
jahier@irisa.fr