HELP TRACE Robert Duncan, December 1994 Tracing function calls during program execution. CONTENTS - (Use g to access required sections) 1 Introduction 2 Compiling in Debug Mode 3 Using the Trace Command 4 Trace Output 5 Tracing Local Functions 6 Global Tracing 7 Exceptions ----------------------------------------------------------------------- 1 Introduction ----------------------------------------------------------------------- Tracing a function allows you to observe all the calls made to that function during the execution of a program. The following transcript of an interactive session shows the steps involved in using the tracer, here to observe the behaviour of the member function: - Compile.debug := true; (* Compile in debug mode *) val it = () : unit - fun member(x, []) = false (* Define a function *) = | member(x, y::ys) = (x = y) orelse member(x, ys); val member : ''a * ''a list -> bool - trace member (* Trace it *) - member(3, [1,2,3,4]); (* Call it; trace follows *) [ 1]> member (3, [1, 2, 3, 4]) [ 1]> member (3, [2, 3, 4]) [ 1]> member (3, [3, 4]) [ 1]< member true val it = true : bool ----------------------------------------------------------------------- 2 Compiling in Debug Mode ----------------------------------------------------------------------- In order to use the tracer you must compile your program with the flag Compile.debug set to true. There are two ways to do this: from ML, by evaluating the expression Compile.debug := true; or from Ved, with the command set ml_compile_debug The *ved_set command toggles the setting of a flag, so you can use this same command to turn debugging off again. The debug flag causes the compiler to plant extra code in each function to provide hooks for the tracer. This obviously makes the code size larger and reduces its execution speed; it also reduces the amount of last-call optimisation which the compiler performs, although functions which are truly tail-recursive remain so. For interactive development work, the overhead is probably sufficiently small that you can have the debug flag set all the time, so that the tracer is always available: you can arrange this by including the line Compile.debug := true; in your "init.ml" file. You should remember, however, to set the flag false again and recompile your program whenever code size or execution speed is important. You can selectively set the debug flag on a per-file basis by using the compiler's localise function: doing Compile.localise Compile.debug := true; will set the flag only for the duration of the current compilation. The previous value of the flag will be restored once the compilation has finished, even if it terminates with an error. See HELP * localise. You should set the debug flag now if you want to try out the examples in this file. ----------------------------------------------------------------------- 3 Using the Trace Command ----------------------------------------------------------------------- To trace a function, use the trace command trace function ... Like all commands, the trace command is recognised only at top-level. The arguments to the command are the names of the functions you want to trace, separated by white space and terminated by a newline. For general information about commands see HELP * COMMANDS. You can only trace functions accessible from the top-level environment, although that includes functions defined within structures if you use long identifiers. Each function must have been defined within a fun declaration, compiled with the Compile.debug flag set to true (the state of the flag is irrelevant when you execute the trace command, or when you run the program). Once a function has been traced, then all calls to that function will be reported on the error output stream, usually the terminal or Ved's interactive window. To stop tracing a function, use the untrace command: untrace function ... Tracing is a property of the function value, so if you redefine a function by recompiling it, the new value is not automatically traced: you must redo the trace command if you want to trace the new definition. As a running example throughout the rest of this file, we will use the following program: Compile.debug := true; fun length([]) = 0 | length(_::xs) = 1 + length(xs) fun member(x, []) = false | member(x, y::ys) = (x = y) orelse member(x, ys); fun filter p ([]) = [] | filter p (x::xs) = if p(x) then x :: filter p xs else filter p xs fun even(n) = (n mod 2 = 0) You should load this program now, and then execute the command trace length member filter The trace command produces no output unless any of its arguments are invalid in some way. ----------------------------------------------------------------------- 4 Trace Output ----------------------------------------------------------------------- Normal trace output contains one line for each function call and one line for the matching return, like this: [ N]> name arguments [ N]< name result Function calls are flagged with the character `>` and returns with `<`. The arguments are the actual arguments of the call and result is the value returned. The call and return lines may not be adjacent if there are intervening calls to other traced functions. The number N is a depth counter which indicates the nesting of traced calls; in long traces, you can use this to match up calls with their returns. For example, calling the function length defined above: length([1,2,3,4]); [ 1]> length [1, 2, 3, 4] [ 2]> length [2, 3, 4] [ 3]> length [3, 4] [ 4]> length [4] [ 5]> length [] [ 5]< length 0 [ 4]< length 1 [ 3]< length 2 [ 2]< length 3 [ 1]< length 4 This function is recursive, so the trace shows one call line and one return line for each of five calls with the trace counter tracking the depth of recursion. With a tail-recursive function, however, all the returns are collapsed to a single line, and the depth counter remains unchanged between calls. You can see this in a call to member: member(3, [1,2,3,4]); [ 1]> member (3, [1, 2, 3, 4]) [ 1]> member (3, [2, 3, 4]) [ 1]> member (3, [3, 4]) [ 1]< member true This reflects how the first two calls of member transfer control entirely to their recursive invocations, with the single result shared by all. In general, wherever you see two calls to the same function with the same depth count and no intervening returns, you can assume that the function is tail recursive. The arguments and results of calls are displayed using the standard top-level printer, so the output can be controlled with the flags described in HELP * PRINTER. However, to prevent trace output from being too verbose, the maximum printing depth is set quite low during tracing. This is controlled by a special flag trace_depth in the Printer module: change this if you want to see more or less output during tracing. As with top-level printing, the trace printer has to know the types of arguments and results before it can display them properly. The tracer tries to deduce appropriate types from the type of the particular application, but this does not always give the most informative results, particularly for local, polymorphic functions or in applications where a type is abstract at the call site. In these cases, the trace may show no more than a dash (-) to stand for a particular argument or result. ----------------------------------------------------------------------- 5 Tracing Local Functions ----------------------------------------------------------------------- Because of the sophisticated scoping rules of SML, much of the work of a program is often done by functions which are not accessible through the top-level environment. These may be functions defined in let or local blocks, or functions not exported from modules. In such cases, tracing only the accessible functions may not provide much useful information. Consider the following redefinition of length to use a local tail- recursive function: fun length(list) = let fun length'([], n) = n | length'(_::xs, n) = length'(xs, n+1) in length'(list, 0) end; trace length (* you must redo this to trace the new version *) In a trace of this function, the work done by the local length' is invisible: length([1,2,3,4]); [ 1]> length [1, 2, 3, 4] [ 1]< length 4 As yet, you cannot trace such hidden functions explicitly. However, you can ask to be shown more of what goes on inside a traced function by qualifying the function name given to the trace command. For this example, we might try: trace length[length'] This says that within any call of length, the tracer should also report on calls to a function named length'. This produces the more informative output: length([1,2,3,4]); [ 1]> length [1, 2, 3, 4] [ 2]> length' ([-, -, -, -], 0) [ 2]> length' ([-, -, -], 1) [ 2]> length' ([-, -], 2) [ 2]> length' ([-], 3) [ 2]> length' ([], 4) [ 2]< length' 4 [ 1]< length 4 You can qualify a trace command argument with any list of names: trace function[name1,name2,...] This means that during a call of the traced function, calls to any other function having one of the given names will also be traced. Names used as qualifiers cut across SML scoping rules: the name of a function -- in this context -- is the identifier which was bound by the fun declaration in which the function was defined. It makes no difference where the binding was done; in particular, the binding doesn't have to be local to the function being traced. The only condition is that the named functions must themselves have been compiled in debug mode. The special qualifier [*] means ``any function'' and turns on full tracing for the duration of the traced function call. One use for this is in tracing higher-order functions. Consider a trace of the function filter defined above: filter even [1,2,3,4,5]; [ 1]> filter fn [1, 2, 3, 4, 5] [ 1]> filter fn [2, 3, 4, 5] [ 2]> filter fn [3, 4, 5] [ 2]> filter fn [4, 5] [ 3]> filter fn [5] [ 3]> filter fn [] [ 3]< filter [] [ 2]< filter [4] [ 1]< filter [2, 4] This shows how filter is collecting a subset of items from its argument list, but not why those particular items are being chosen. We can see more by adjusting the trace on filter to include all its nested calls: trace filter[*] filter even [1,2,3,4,5]; [ 1]> filter fn [1, 2, 3, 4, 5] [ 2]> even 1 [ 2]< even false [ 1]> filter fn [2, 3, 4, 5] [ 2]> even 2 [ 2]< even true [ 2]> filter fn [3, 4, 5] [ 3]> even 3 [ 3]< even false [ 2]> filter fn [4, 5] [ 3]> even 4 [ 3]< even true [ 3]> filter fn [5] [ 4]> even 5 [ 4]< even false [ 3]> filter fn [] [ 3]< filter [] [ 2]< filter [4] [ 1]< filter [2, 4] An important point to understand here is that the function even is not being traced explicitly, but only within the call to filter. You can turn off local tracing with the untrace command: untrace length[length'] filter[*] ----------------------------------------------------------------------- 6 Global Tracing ----------------------------------------------------------------------- You can use a list of qualifiers alone as an argument to the trace command: trace [name1,name2,...] This turns on tracing for all functions called name1, name2, etc., wherever they are defined and wherever they are called. This may be the only way to get a trace on a function hidden in a module somewhere, but you can use it to trace accessible functions too: trace [length,member,filter] The advantage of doing things this way is that, because these qualifiers don't respect scoping rules, you can redefine any of the named functions and still keep a trace on them without having to redo the trace command. The disadvantages are that execution time will be further increased, and that any other locally-defined function which happens to have the same name as one of these will also appear in the trace, and may be difficult to distinguish. The command trace [*] turns on full, global tracing, such that everything that can be traced, will be. This can produce a considerable quantity of output. ----------------------------------------------------------------------- 7 Exceptions ----------------------------------------------------------------------- Within a call to a traced function, the exception traceback feature is automatically enabled so that exceptions will show up in the trace output. An exception appears in trace output as a sequence [ N]E exception [ N]X name . . . [ N]H name The character `E` in the first line indicates the raising of an exception; the exception packet is displayed. Lines marked `X` show function calls abandoned as a result of the exception: all discarded calls are printed, but exits from traced calls are indicated by changes in the trace counter N. The `H` line appears only if the exception is handled; normal tracing is resumed thereafter. See HELP * traceback. --- C.all/pml/help/trace --- Copyright University of Sussex 1994. All rights reserved.