• No results found

Recording and replaying execution

In document Interactive functional programming (Page 51-53)

Bernstein and Stark [BS95] suggest that a debugger should be seen as a general-purpose tool for observing the behaviour of a program according to some well-defined operational model. We take a similar view of traces: they should record the behaviour of the program according to a specific operational model, which we call the reference semantics. In particular, as we mention in §2.3, traces should not distinguish executions which the reference semantics regards as equivalent. Thus low-level tracing techiques that expose implementation details, such as those used some in time-travel virtual machines [Lew03,KDC05], are not relevant here.

The structure of traces depends on the chosen reference semantics, then, and can be considered inde- pendently of methods for building them. For a big-step semantics, the traces can to a first approximation simply be the finite derivation trees for the evaluation judgement. In other words, interpreting the evaluation judgement as a signatureΣ, a suitable data type of traces is the initial Σ-algebra. A small-step (transition) semantics does not immediately give rise to an inductive data type; a trace format widely used in this set- ting has been the redex trail of Sparud and Runciman [SR97], later enhanced into the augmented redex trail (ART) of Wallace et al. [WCBR01]. The formal correctness of redex trails with respect to the original tran- sition semantics was neglected until Brassel’s work on redex trails for lazy logic languages [BHHV04] and Chitil and Luo’s formalisation of ART traces [CL07].

The main difference between ART traces and the traces we define in Chapter4is that an ART records a small-step term-rewriting derivation, rather than a big-step derivation. ARTs have generally been used for lazy languages, where graph-rewriting approaches are common [Wad71], but the trace format itself is flexible enough to support other reduction strategies. The contraction of a redex is recorded by the addition of a new node for the reduct, with an edge linking it to the redex, rather than contracting the redex in situ as would normally happen in graph reduction. The fact that the ART is only ever added to is similar to how our traces are an inflation or “unrolling” of the source program. Moreover Silva and Chitil use an ART for a system which combines algorithmic debugging with fine-grained program slicing [SC06]; we contrast this with our slicing approach in §3.4below.

Execution under a small-step semantics can also be recorded into a trace with a big-step shape; a well- known example is the evaluation dependence tree (EDT) of Nilsson and Sparud [NS96], which we discuss in more detail in §3.3below. The authors informally compare an EDT to a big-step call-by-value derivation where unneeded arguments remain unevaluated. As our system does, the EDT employs an environment-

based semantics to record source names of variables.

Now we turn to methods for building traces. The general idea is to observe the execution in some way, and then transcribe the observations into a trace. One approach is a source-to-source technique that transforms the program to be traced into an instrumented, “self-tracing” version that builds the trace as it executes. The trace describes the execution of the uninstrumented program. This approach has been used widely in debuggers for both lazy languages [CRC+03,SN95,BJ97] and strict languages [TA95]. Since the tracing code is part of the user program, there is no possibility of its inadvertently depending on implementation details of the interpreter. Moreover the instrumented program can be executed on any interpreter for the language. An alternative approach to tracing is to instrument the interpreter instead of the program. While evaluat- ing the program, the instrumented interpreter builds a description of the evaluation that would have taken place under the reference interpreter. Chitil [Chi01] and Brassel [ibid.] both use this technique to record redex trails.

Kishon et al. [KHC91] describe a more abstract approach, which they present in a denotational setting. Their technique can be used for applications other than tracing, and works for any language for which a continuation semantics can be given. From the continuation semantics, they derive a monitoring semantics, which is parameterised by a monitor state and a set of monitoring functions. The monitoring semantics threads the monitoring state through the computation; the monitoring functions are used to transform the monitoring state at each point where control is transferred to the continuation. Thus monitoring semantics are a general, denotational approach to observing execution. The approach can be used to construct a trace by using a “tracing monitor” whose job it is to record the observations. Kishon et al. also show how a monitoring interpreter can be optimised into an instrumented interpreter by partially evaluating with respect to a particular monitor, and into an instrumented program by partially evaluating with respect to a particular program. Thus their approach is powerful enough to subsume both instrumentation approaches mentioned above.

We do not need the generality of Kishon et al., and so the approach we take to tracing in this thesis is to use an instrumented interpreter. The traces we build are, roughly speaking, derivation trees for a big- step reference semantics (Chapter 4). Derivation trees “proper” carry environments, contexts, and other meta-values as indices; we omit many of these details from our traces. We also do not formally establish a correspondence between traces and big-step derivations. Instead, the correctness property that our traces satisfy is that they contain enough information to be able to run the computation backwards, recovering enough of the original program to compute the value whose computation was traced (§5.3.2). How this relates to other notions of trace correctness is a topic for future study.

Given a trace, we often want to recover a “stepping” or “focused” view of the execution, allowing the user to recover the individual steps recorded in the trace. We call this replaying. If the trace is a small-step trace, such as an ART, then it is possible to replay small-step reduction steps directly from the trace, as for example described by Chitil and Luo [CL07]. When the trace is a big-step derivation tree, there is no explicit notion of evaluation “step” to recover. Da Silva shows how to derive a stepping transition system from a big-step semantics [dS91]; this can be useful for showing how debuggers correspond to a big-step reference semantics. For example da Silva goes on to prove the correctness of a stepping debugger by exhibiting a

bisimulation between his derived transition system and the debugger.

A trace affords a spatial, “all at once” view of a computation. Replaying it recovers the temporal, step- ping view of the computation from which the trace was recorded. Indeed, record and replay relate these two perspectives to each other: a trace can be “replayed” as a stream of observations of the computation; and any finite prefix of such a stream can be “recorded” into a trace. Victor argues convincingly for the importance of being able to switch freely between these perspectives [Vic11]. For systems that provide the temporal, stepping view, basing that on trace replay means that execution can be explored after the program has run and the focus moved freely backwards and forwards in time. On the other hand, traces can be expensive to create and store; we consider efficient ways of representing traces in Future Work, §7.2.1. It is possible to provide a temporal, stepping view without an underlying trace, by simply allowing execution to be suspended and resumed by the user. However it is not then possible to step backwards, because at each transition information is lost about prior states. We discuss ways of presenting execution based on all of these approaches in §3.6below.

In document Interactive functional programming (Page 51-53)