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.
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:
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 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.
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.
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:
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 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):
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 .
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.
:- 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.
The integration of the command into a window-based user interface can be done according to the entry in the interface slot:
Currently, there is no Window User Interface for Morphine.
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):
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.
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):
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.
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):
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.
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.
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
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:
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.
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:
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:
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.
The scenario handler gives an error message in the following cases:
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:
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.
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.