Sometimes, programmers find themselves needing to perform some side-effects in the middle of declarative code. One example is an operation that takes so long that users may think the program has gone into an infinite loop: periodically printing a progress message can give them reassurance. Another example is a program that is too long-running for its behaviour to be analyzed via debuggers and too complex for analysis via profilers; a programmable logging facility generating data for analysis by a specially-written program may be the best option. However, inserting arbitrary side effects into declarative code is against the spirit of Mercury. Trace goals exist to provide a mechanism to code these side effects in a disciplined fashion.
The format of trace goals is
trace Params Goal.
Goal must be a valid goal;
Params must be a valid list of one or more trace parameters.
The following example shows all four of the available kinds of parameters:
‘compile_time’, ‘run_time’, ‘io’ and ‘state’.
(In practice, it is far more typical to have just one parameter, ‘io’.)
:- mutable(logging_level, int, 0, ground, ). :- pred time_consuming_task(data::in, result::out) is det. time_consuming_task(In, Out) :- trace [ compile_time(flag("do_logging") or grade(debug)), run_time(env("VERBOSE")), io(!IO), state(logging_level, !LoggingLevel) ] ( io.write_string("Time_consuming_task start\n", !IO), ( !.LoggingLevel > 1 -> io.write_string("Input is ", !IO), io.write(In, !IO), io.nl(!IO) ; true ) ), … % perform the actual task
The ‘compile_time’ parameter says under what circumstances the trace goal should be included in the executable program. In the example, at least one of two conditions has to be true: either this module has to be compiled with the option ‘--trace-flag=do_logging’, or it has to be compiled in a debugging grade.
In general, the single argument of the ‘compile_time’ function symbol is a boolean expression of primitive compile-time conditions. Valid boolean operators in these expressions are ‘and’, ‘or’ and ‘not’. There are three kinds of primitive compile-time conditions. The first has the form ‘flag(FlagName)’, where FlagName is an arbitrary name picked by the programmer; this condition is true if the module is compiled with the option ‘--trace-flag=FlagName’. The second has the form ‘tracelevel(shallow)’, or ‘tracelevel(deep)’; this condition is true (irrespective of grade) if the module is compiled with at least the specified trace level. The third has the form ‘grade(GradeTest)’. The supported ‘GradeTests’s and their meanings are as follows.
True if the module is compiled with execution tracing enabled.
True if the module is compiled with source-to-source debugging enabled.
True if the module is compiled with non-deep profiling enabled.
True if the module is compiled with deep profiling enabled.
True if the module is compiled with parallel execution enabled.
True if the module is compiled with trailing enabled.
True if the module is compiled with ‘--highlevel-code’ disabled.
True if the module is compiled with ‘--highlevel-code’ enabled.
True if the target language of the compilation is C.
True if the target language of the compilation is C#.
True if the target language of the compilation is Java.
The ‘run_time’ parameter says under what circumstances the trace goal, if included in the executable program, should actually be executed. In this case, the environment variable ‘VERBOSE’ has be to set when the program starts execution. (It doesn’t matter what value it is set to.)
In general, the single argument of the ‘run_time’ function symbol is a boolean expression of primitive run-time conditions. Valid boolean operators in these expressions are ‘and’, ‘or’ and ‘not’. There is just one primitive run-time condition. It has the form ‘env(EnvVarName)’, this condition is true if the environment variable EnvVarName exists when the program starts execution.
The ‘compile_time’ and ‘run_time’ parameters may not appear in the parameter list more than once; programmers who want more than one condition have to specify how (with what boolean operators) their values should be combined. However, it is ok for them not to appear in the parameter list at all. If there is no ‘compile_time’ parameter, the trace goal is always compiled into the executable; if there is no ‘run_time’ parameter, the trace goal is always executed (if it is compiled into the executable).
Since the trace goal may end up either not compiled into the executable or just not executed, it cannot bind any variables that occur in the surrounding code. (If it were allowed to bind such variables, then those variables would stay unbound if either the compile time or the run time condition were false.) This greatly restricts what trace goals can do.
The usual reason for including a trace goal in a procedure body is to perform some I/O, which requires access to the I/O state. The ‘io’ parameter supplies this access. Its argument must be the name of a state variable prefixed by ‘!’; by convention, it is usually ‘!IO’. The language implementation supplies the initial unique value of the I/O state as the value of ‘!.IO’ at the start of the trace goal; it requires the trace goal to give back the final unique value of the I/O state as the value of ‘!.IO’ current at the end of the trace goal.
Note that trace goals that wish to do I/O must include this parameter in their parameter list even if the surrounding code already has access to an I/O state. This is because otherwise, doing any I/O inside the trace goal would destroy the value of the current I/O state, changing the instantiation state of the variable holding it, and trace goals are not allowed to do that.
The ‘io’ parameter may appear in the parameter list at most once, since it doesn’t make sense to have two copies of the I/O state available to the trace goal.
Besides doing I/O, trace goals may read and possibly write the values of mutable variables. Each mutable the trace goal wants access to should be listed in its own ‘state’ parameter (which may therefore appear in the parameter list more than once). Each ‘state’ parameter has two arguments: the first gives the name of the mutable, while the second must be the name of a state variable prefixed by ‘!’, e.g. ‘!LoggingLevel’. The language implementation supplies the initial value of the mutable as the value of (in this case) ‘!.LoggingLevel’ at the start of the trace goal; at the end of the trace goal, it puts the value of ‘!.LoggingLevel’ current then back into the mutable.
The intention here is that trace goals should be able to access mutables that give them information about the parameters within which they should operate. The ability of trace goals to actually update the values of mutables is intended to allow the implementation of trace goals whose actions depend on the actions executed by previous trace goals. For example, a trace goal could test whether the current input value is the same as the previous input value, and if it is, then it can say so instead of printing the value out again. Another possibility is a progress message which is printed not for every item processed, but for every 1000th item, reassuring users without overwhelming them with a deluge of output.
This kind of code is the only intended use of this ability. Any program in which the value of a mutable set by a trace goal is inspected by code that is not itself within a trace goal is explicitly violating the intended uses of trace goals. Only the difficulty of implementing the required global program analysis prevents the language design from outlawing such programs in the first place.
The compiler will not delete trace goals from the bodies of the procedures containing them. However, trace goals inside a procedure don’t prevent calls to that procedure from being optimized away, if such optimization is otherwise possible. (There is no point in debugging or logging operations that don’t actually occur.) In their effect on program optimizations, trace goals function as a kind of impure code, but one with an implicit promise_pure around the clause in which they occur.