Reversible debugging allows one to explore a program execution back and forth. In particular, if one observes a misbehaviour in some execution (e.g., a variable that takes a wrong value or an unexpected exception), reversible debugging allows us to analyse the execution backwards from this point. This feature is particularly useful for long executions, where a step-by-step forward inspection from the beginning of the execution would take too much time, or be even impractical.
One can already find a number of tools for reversible debugging in different programming languages, like Undo [UndoWhitePaper], rr [OJFHNP17] or CauDEr [LPV19], to name a few. In this work, we consider reversible debugging in logic programming [Llo87]. In this context, one has to deal with two specific features that are not common in other programming languages: nondetermism and a bidirectional parameter passing mechanism (unification).
Typically, the reversibilization of a (reduction) semantics can be obtained by instrumenting the states with an appropriate Landauer embedding [Lan61], i.e., by introducing a history where the information required to undo the computation steps is stored. Defining a Landauer embedding for logic programming is a challenging task because of nondetermism and unification. On the one hand, in order to undo backtracking steps, a deterministic semantics that models the complete traversal of an SLD tree is required (like the linear operational semantics introduced in [SESGF11]). On the other hand, unification is an irreversible operation: given two terms, and , with most general unifier , we cannot obtain from and (nor from and ).
Let us note that, in this work, we aim at reversibility in the sense of being able to deterministically undo the steps of a computation. In general, (pure) logic programs are invertible (e.g., the same relation can be used for both addition and subtraction), but they are not reversible in the above sense.
This paper extends the preliminary results reported in the short paper [Vid20]. In particular, our main contributions are the following:
First, we define a reversible operational semantics for logic programs that deals explicitly with backtracking steps. In particular, we define both a forward and a backward transition relation that model forward and backward computations, respectively.
Moreover, we state and prove some formal properties for our reversible semantics, including the fact that it is indeed a conservative extension of the standard semantics, that it is deterministic, and that any forward computation can be undone.
Finally, we present the design of a reversible debugger for Prolog that is based on our reversible semantics, and discuss some aspects of the implemented tool, the reversible debugger rever.
We consider that our work can be useful in the context of existing techniques for program validation in logic programming, like run-time verification (e.g., [SMH14]) or concolic testing (e.g., [MPV15]), in order to help locating the bugs of a program.
The paper is organised as follows. After introducing some preliminaries in the next section, we introduce our reversible operational semantics in Section 3. Then, Section 4 presents the design of a reversible debugger based on the previous semantics. Finally, Section 5 compares our approach with some related work and Section 6 concludes and points out some directions for further research.
In this section, we briefly recall some basic notions from logic programming (see, e.g., [Llo87, Apt97] for more details).
In this work, we consider a first-order language with a fixed vocabulary of predicate symbols, function symbols, and variables denoted by , and , respectively, with and . Every element of has an arity which is the number of its arguments. We write (resp. ) to denote that (resp. ) is an element of (resp. ) whose arity is . A constant symbol is an element of whose arity is 0. We let denote the set of terms constructed using symbols from and variables from .
An atom has the form with and for . A query is a finite conjunction of atoms which is denoted by a sequence of the form , where the empty query is denoted by . A clause has the form , where (the head) and (the body) are atoms, (thus we only consider definite logic programs, i.e., logic programs without negated atoms in the body of the clauses). Clauses with an empty body, , are called facts, and are typically denoted by . In the following, atoms are ranged over by while queries (possibly empty sequences of atoms) are ranged over by
denotes the set of variables in the syntactic object (i.e., can be a term, an atom, a query, or a clause). A syntactic object is ground if . In this work, we only consider finite ground terms.
Substitutions and their operations are defined as usual; they are typically denoted by (finite) sets of bindings like, e.g., . We let denote the identity substitution. Substitutions are ranged over by In particular, the set is called the domain of a substitution . Composition of substitutions is denoted by juxtaposition, i.e., denotes a substitution such that for all . We follow a postfix notation for substitution application: given a syntactic object and a substitution the application is denoted by . The restriction of a substitution to a set of variables is defined as follows: if and otherwise. We say that if .
A syntactic object is more general than a syntactic object , denoted , if there exists a substitution such that . A variable renaming is a substitution that is a bijection on . Two syntactic objects and are variants (or equal up to variable renaming), denoted , if for some variable renaming . A substitution is a unifier of two syntactic objects and iff ; furthermore, is the most general unifier of and , denoted by if, for every other unifier of and , we have that .
A logic program is a finite sequence of clauses. Given a program , we say that is an SLD resolution step111In this paper, we only consider Prolog’s computation rule, so that the selected atom in a query is always the leftmost one. if is a renamed apart clause (i.e., with fresh variables) of program , in symbols, , and . The subscript will often be omitted when the program is clear from the context. An SLD derivation is a (finite or infinite) sequence of SLD resolution steps. As is common, denotes the reflexive and transitive closure of . In particular, we denote by a derivation
where if (and otherwise).
An SLD derivation is called successful if it ends with the query , and it is called failed if it ends in a query where the leftmost atom does not unify with the head of any clause. Given a successful SLD derivation , the associated computed answer, , is the restriction of to the variables of the initial query . SLD derivations are represented by a (possibly infinite) finitely branching tree, which is called SLD tree. Here, choice points (queries with more than one child) correspond to queries where the leftmost atom unifies with the head of more than one program clause.
Consider the following (labelled) logic program:222We consider Prolog notation in examples (so variables start with an uppercase letter). Clauses are labelled with a unique identifier of the form .
Given the query , we have, e.g., the following (successful) SLD derivation:
with computer answer .
3 A Reversible Semantics for Logic Programs
In this section, we present a reversible semantics for logic programs that constitutes an excellent basis to implement a reversible debugger for Prolog (cf. Section 4). In principle, one of the main challenges for defining a reversible version of SLD resolution is dealing with unification, since it is an irreversible operation. E.g., given the SLD resolution step
using clause , there is no deterministic way to get back the query from the query , the computed mgu , and the applied clause. For instance, one could obtain the query since the following SLD resolution step
is also possible using the same clause and computing the same mgu.
In order to overcome this problem, [Vid20] proposed a reversible semantics where
computed mgu’s are not applied to the atoms of the query, and
the selected call at each SLD resolution step is also stored.
In [Vid20], queries as represented as pairs , where the first component is a sequence of atoms (a query), and the second component stores, for each SLD resolution step performed so far, the selected atom (), the head of the selected clause (), and the number of atoms in the body of this clause (). Here, mgu’s are not stored explicitly but can be inferred from the pairs . The number is used to determine the number of atoms in the current query that must be removed when performing a backward step. A reversible SLD resolution step has then the form333Here, denotes a list with head element and tail .
if there exists a clause and , where is the substitution obtained from by computing the mgu’s associated to each triple ( in and, then, composing them. A simple proof-of-concept implementation that follows this scheme can be found at https://github.com/mistupv/rever/tree/rc2020.
The proposal in [Vid20], however, suffers from several drawbacks:
First, it is very inefficient, since one should compute the mgu’s of each SLD resolution step once and again. This representation was chosen in [Vid20] for clarity and, especially, because it allowed us to easily implement it without using a ground representation for queries and programs, so that there was no need to reimplement all basic operations (mgu, substitution application and composition, etc).
The second drawback is that the above definition of reversible SLD resolution cannot be used to undo a backtracking step, since the structure of the SLD tree is not explicit in the considered semantics.
In the following, we introduce a reversible operational semantics for logic programs that overcome the above shortcomings.
3.1 A Deterministic Operational Semantics
First, we present a deterministic semantics (inspired by the linear operational semantics of [SESGF11]) that deals explicitly with backtracking.
Our semantics is defined as a transition relation on states. In the following, queries are represented as pairs instead of , where is the composition of the mgu’s computed so far in the derivation. This is needed in order to avoid undoing the application of mgu’s, which is an irreversible operation.
Definition 1 (state)
A state is denoted by a sequence , where each is a (possibly labelled) query of the form . In some cases, a query is labelled with a clause label, e.g., , which will be used to denote that the query can be unfolded with the clause labelled with (see below).
A state will often be denoted by so that is the first query of the sequence and denotes a (possibly empty) sequence of queries. In the following, an empty sequence is denoted by .
In this paper, we consider that program clauses are labelled, so that each label uniquely identifies a program clause. Here, we use the auxiliary function to obtain the labels of those clauses in program whose heads unify with atom , i.e.,
and to get a renamed apart variant of the clause labelled with , i.e., if and is a variable renaming with fresh variables.
The rules of the semantics can be found in Figure 1. An initial state has the form , where is an atom, is a (possibly empty) sequence of atoms, and is the identity substitution. Initially, one can either apply rule choice or choice_fail. Let us assume that unifies with the head of some clauses, say . Then, rule choice derives a new state by replacing with copies labelled with :
Now, let assume that returns . Then, rule unfold applies so that the following state is derived:
Let us consider now that does not match any program clause, i.e., we have . Then, rule choice_fail applies and the following state is derived:
Then, rule backtrack applies so that we jump to a choice point with some pending alternative (if any). In this case, we derive the state
so that unfolding with clause is tried now, and so forth.
Here, we say that a derivation is successful if the last state has the form . We have also included a rule called next to be able to reach all solutions of an SLD tree (which has a similar effect as rule backtrack). Therefore, is not necessarily the first computed answer, but an arbitrary one (as long as it is reachable from the initial state after a finite number of steps).
A computation is failed if it ends with a state of the form , so no rule is applicable (note that rule backtrack is not applicable when there is a single query in the state).
Consider the program of Example 1 and the same initial query: . In order to reach the same computed answer, , we now perform the following (deterministic) derivation:444For clarity, we only show the bindings for the variables in the initial query. Moreover, the steps are labelled with the applied rule.
with computer answer .
Clearly, the semantics in Figure 1 is indeed deterministic. In the following, we assume a fixed program is considered for stating formal properties.
Let be a state. Then, at most one rule from the semantics in Figure 1 is applicable.
The proof is straightforward since the conditions of the rules do not overlap:
If the leftmost query is not headed by nor and the query is not labelled, only rule choice and choice_fail are applicable, and the conditions trivially do not overlap.
If the leftmost query is labelled, only rule unfold is applicable.
Finally, if the leftmost query is headed by (resp. ) then only rule backtrack (resp. next) is applicable.
Now, we prove that the deterministic operational semantics is sound in the sense that it explores the SLD tree of a query following Prolog’s depth-first search strategy:
Let be an initial state. If , then , up to variable renaming.
Here, we prove a more general claim. Let us consider an arbitrary query, with , where is either or , . Then, we have for all such that for some , up to variable renaming. We exclude the queries with since failures are not made explicit in the definition of SLD resolution (this is just a device of our deterministic semantics to point out that either a backtracking step should be performed next or the derivation is failed).
We prove the claim by induction on the number of steps in the former derivation: . Since the base case () is trivial, let us consider the inductive case (). Here, we assume a derivation of steps from . By the induction hypothesis, we have for all such that for some . We now distinguish several possibilities depending on the applied rule to the state :
If the applied rule is backtrack or next, we have
and the claim trivially holds by the induction hypothesis.
If the applied rule is choice, we have
for some , and the claim also follows trivially from the induction hypothesis.
If the applied rule is choice_fail, the claim follows immediately by the induction hypothesis since a query of the form is not considered.
Finally, let us consider that the applied rule is unfold. Let . Then, we have
if and . Then, we also have an SLD resolution step of the form using the same clause555For simplicity, we assume that the same renamed clauses are considered in both derivations. and computing the same mgu and, thus, the claim follows from the induction hypothesis.
Note that the deterministic semantics is sound but incomplete in general since it implements a depth-first search strategy.
3.2 A Reversible Semantics
Now, we extend the deterministic operational semantics of Figure 1 in order to make it reversible. Our reversible semantics is defined on configurations:
Definition 2 (configuration)
A configuration is defined as a pair where is a state (as defined in Definition 1) and is a list representing the history of the configuration. Here, we consider the following history events:
: denotes a choice step with branches;
: represents an unfolding step where the selected atom is , the answer computed so far is , and the selected clause is labelled with ;
: is associated to rule choice_fail and denotes that the selected atom matches no rule;
: denotes that the execution of atom has been completed (see below);
: represents a backtracking step, where is the query that failed;
: denotes an application of rule after an answer is obtained.
We use Haskell’s notation for lists and denote by a history with first element and tail ; an empty history is denoted by .
The reversible (forward) semantics is shown in Figure 2.666The subscripts of some configurations: call, exit, fail, redo, and answer, can be ignored for now. They will become useful in the next section. The rules of the reversible semantics are basically self-explanatory. They are essentially the same as in the standard deterministic semantics of Figure 1 except for the following differences:
First, configurations now keep a history with enough information for undoing the steps of a computation.
And, secondly, unfolding an atom now adds a new call of the form after the atoms of the body (if any) of the considered program clause. This is then used in rule exit in order to determine when the call has been completed successfully ( marks the exit of a program clause). This extension is not introduced for reversibility, but it is part of the design of our reversible debugger (see Section 4, where the reversible debugger rever is presented). Here, and in the following, we assume that programs contain no predicate named .
We note that extending our developments to SLD resolution with an arbitrary computation rule (i.e., different from Prolog’s rule, which always selects the leftmost atom) is not difficult. Basically, one would only need to extend the elements as follows: , where is the position of the selected atom in the current query.
It is worthwhile to observe that the drawbacks of [Vid20] mentioned before are now overcome by using substitutions with the answer computed so far, together with a deterministic semantics where backtracking is dealt with explicitly.
Trivially, the instrumented semantics of Figure 2 is a conservative extension of the deterministic semantics of Figure 1 since the rules impose no additional condition. The only difference is the addition of atoms that mark the exit of a program clause. In the following, given two states, , we let if they are equal after removing all atoms of the form .
Let be an initial state. Then, iff such that for some history , up to variable renaming.
Let us now consider backward steps. Here, our goal is to be able to explore a given derivation backwards. For this purpose, we introduce a backward operational semantics that is essentially obtained by switching the configurations in each rule of the forward semantics, and removing all unnecessary premises. The resulting backward semantics is shown in Figure 4. Let us just add that, in rule , we use the auxiliary function to denote the body of clause labelled with in program , and, thus, represents the number of atoms in the body of this clause.777 As is common, denotes the cardinality of the set or sequence . This information was stored explicitly in our previous approach [Vid20].
The following result states the reversibility of our semantics:
Let be configurations. If , then , up to variable renaming.
The claim follows by a simple case distinction on the applied rule and the fact that the backward semantics of Figure 4 is trivially deterministic since each rule requires a different element on the top of the history.
In principle, one could also prove the opposite direction, i.e., that implies , by requiring that is not an arbitrary configuration but a “legal” one, i.e., a configuration that is reachable by a forward derivation starting from some initial configuration.
The above result could be straightforwardly extended to derivations as follows:
Let be configurations. If , then , up to variable renaming.
4 A Reversible Debugger for Prolog
In this section, we present the design of a reversible debugger for Prolog. It is based on the standard 4-port tracer introduced by Byrd [Byr80, CM94]. The ports are (an atom is called), (a call is successfully completed), (backtracking requires trying again some call), and (an atom matches no clause). In contrast to standard debuggers that can only explore a computation forward, our reversible debugger allows us to move back and forth.
The implemented debugger, rever, is publicly available from https://github.com/mistupv/rever. It can be used in two modes:
Debug mode. In this case, execution proceeds silently (no information is shown) until the execution of a special predicate is reached (if any). The user can include a call to this predicate in the source program in order to start tracing the computation (i.e., it behaves as trace/0 in most Prolog systems). Tracing also starts if an exception is produced during the evaluation of a query. This mode is invoked with a call of the form , where is the initial query whose execution we want to explore.
Trace mode. In this mode, port information is shown from the beginning. One can invoke the trace mode with . Note that it is equivalent to calling .
Our reversible debugger essentially implements the transition rules in Figures 2 and 4. As the reader may have noticed, some configurations in Figure 2 are labeled with a subscript: it denotes the output of a given port. Moreover, there is an additional label in rule which denotes that, at this point, an answer must be shown to the user.
In tracing mode, every time that a configuration with a subscript is reached, the execution stops, shows the corresponding port information, and waits for the user to press some key. We basically consider the following keys: (or Enter) proceeds with the next (forward) step; performs a backward step; (for skip) shows the port information without waiting for any user interaction; enters the tracing mode; quits the debugging session.
For instance, given the initial call , and according to the forward derivation shown in Figure 3, our debugger displays the sequence shown in Figure 5 (a). Now, if one presses “” repeatedly, the sequence displayed in Figure 5 (b) is shown. Note that ports are prefixed by the symbol “” in backward derivations. Of course, the user can move freely back and forth.
Reversible debugging might be especially useful when we have a long execution that produces some exception at the end. With our tool, one can easily inspect the execution backwards from the final state that produced the error.
Let us mention that, in order to avoid the use of a ground representation and having to implement all basic operations (mgu, substitution application and composition, etc), substitutions are represented in its equational form. E.g., substitution is represented by . This equational representation of a mgu can be easily obtained by using the predefined predicate . This representation is much more efficient than storing pairs of atoms (as in [Vid20]), that must be unified once and again at each execution step.
Finally, let us mention that, despite the simplicity of the implemented system (some 500 lines of code in SWI Prolog), our debugger is able to deal with medium-sized programs (e.g., it has been used to debug the debugger itself).
5 Related Work
The closest approach is clearly the preliminary version of this work in [Vid20]. There are, however, several significant differences: [Vid20] presents a reversible version of the usual, nondeterministic SLD resolution. Therefore, backtracking steps cannot be undone. This is improved in this paper by considering a deterministic semantics that models the traversal of the complete SLD tree. Moreover, [Vid20] considers a simple but very inefficient representation for the history, which is greatly improved in this paper. Finally, we provide proofs of some formal properties for our reversible semantics, as well as a publicly available implementation of the debugger, the system rever.
Another close approach we are aware of is that of Opium [Duc99], which introduces a trace query language for inspecting and analyzing trace histories. In this tool, the trace history of the considered execution is stored in a database, which is then used for trace querying. Several analysis can then be defined in Prolog itself by using a set of given primitives to explore the trace elements. In contrast to our approach, Opium is basically a so-called “post-mortem” debugger that allows one to analyze the trace of an execution. Therefore, the goal is different from that of this paper.
6 Concluding Remarks and Future Work
We have proposed a novel reversible debugging scheme for logic programs by defining an appropriate Landauer embedding for a deterministic operational semantics. Essentially, the states of the semantics are extended with a history that keeps track of all the information which is needed to be able to undo the steps of a computation. We have proved a number of formal properties for our reversible semantics. Moreover, the ideas have been put into practice in the reversible debugger rever, which is publicly available from https://github.com/mistupv/rever. Our preliminary experiments with the debugger have shown promising results.
As for future work, we are currently working on extending the debugger in order to cope with negation and the cut. Also, we plan to define a more compact representation for the history, so that it can scale up better to larger programs and derivations.