A Slicer for Functional Logic Programs


 

 


Abstract

 

 

 

Program slicing is a well-known technique that has been widely used for debugging in the context of imperative programming. Despite the fact that debugging is a particularly difficult task within the lazy declarative languages, one can find very few approaches to program
slicing in this context. In order to overcome this problem, we have implemented a slicing tool for a first-order, lazy functional logic language. In this page we describe the results obtained from some experiments and put available the source code of the tool.

 

 


Dynamic Program Slicing in Functional Logic Languages

 

 

Modern functional logic languages like Curry and Toy combine the most important features of functional logic languages, e.g., lazy evaluation and non-determinism. Due to the complexity of their operational semantics, following the actual trace of a computation is often useless. In the context of (purely) functional lazy languages like Haskell, this drawback is overcome by introducing higher-level models that hide the details of lazy evaluation (e.g., Hood, Freja, Buddha, and Hat). Many of these approaches are based on the construction of a redex trail, a directed graph which records copies of all values and redexes reducible expressions) of a computation, with a backward link from each reduct to the parent redex that created it.

 

Recently, Brassel et al introduced an instrumented version of an operational semantics for the kernel of lazy functional logic languages that returns, not only the computed values and bindings, but also a trail of the computation. As in the Augmented Redex Trail approach, this trail can be used to perform tracing and algorithmic debugging, as well as to observe data structures through a computation. Moreover, the correctness of the computed trail is formally proved, which amounts to say that it contains all (and only) the reductions performed in a computation.

While tracing-based debugging provides a powerful tool for inspecting erroneous computations, the task still remains complex. A well-known debugging technique for imperative programs is based on slicing [1], a method for decomposing programs by analyzing their data and control flow. Roughly speaking, a program slice consists of those program statements which are (potentially) related with the values computed at some program point and/or variable, referred to as a slicing criterion. Program slices are usually computed from a program dependence graph that makes explicit both the data and control dependences for each operation in a program. Program dependences can be traversed backwards or forwards (from the slicing criterion), which is known as backward or forward slicing, respectively. Additionally, slices can be dynamic or static, depending on whether a concrete program's input is provided or not. A complete survey on slicing can be found, e.g., in [2].

When debugging programs, we usually start from a particular computation that outputs an incorrect value. In this situation, dynamic slicing may help the programmer to find the location of the bug by extracting a program slice containing only the sentences whose execution influences the incorrect value. Therefore, we are mainly interested in dynamic slicing which only reflects the actual dependences of the erroneous execution, thus producing smaller -more precise- slices than static slicing.

To the best of our knowledge, there is no dynamic slicing tool for lazy languages like Haskell or Curry. Therefore, our original motivation was to fill this gap with a formal technique for dynamic slicing in lazy functional logic languages (nevertheless, the basic ideas also apply to pure lazy functional languages like Haskell). Rather than starting from scratch, our technique relies on a slight extension of redex trails. Basically, we extend the redex trail model in order to also store the location, in the program, of each reduced expression. Then, we compute a dynamic slice by first gathering the reachable nodes of the redex trail -according to the slicing criterion- and, then, deleting those program expressions which are not related to the collected nodes.

From our point of view, dynamic slicing may provide a complementary -rather than alternative- approach to tracing. A clear advantage of our approach is that one can enhance existing tracers with slicing capabilities with a modest implementation effort, since the same data structure -the redex trail- is used for both tracing and slicing.

More information about this schema can be found here.

 

 


The Slicer

 

 

The slicer is divided in the following modules:

Tracer: Firstly, a program is executed with an incorporated instrumented interpreter which, as a side effect, stores the redex trail of the computation in a file. The tracer is implemented in Haskell and accepts lazy, first-order, functional-logic programs that can be traced either backwards or forwards. Here, we have extended the original tracer in order to also store program positions in the nodes of the redex trail. The extension was not difficult and can also be done with other tracers based on redex trails.

Viewer: Once the computation terminates (or it is aborted if it does not terminate), the viewer reads the file with the redex trail and allows the user to navigate through the entire computation. The viewer is implemented in Curry, a conservative extension of Haskell with features from logic programming including logical variables and non-determinism. In this case, no extension was required.

Slicer: Given a redex trail and a slicing criterion, the slicer outputs a set of program positions that uniquely identify the associated program slice. The slicing tool is implemented in Curry, and includes an editor that shows the original program and, whenever a slice is computed, it highlights the expressions belonging to the computed slice (see figure below).

The Slicing Tool

Specializer: For a given initial call, the specializer extracts an executable specialized version of the program w.r.t. this call. This specialized version can be further simplified with a post-process of amorphous slicing.

 

 


Implementation

 

 

We have conducted some experiments in order to measure the performance of the tool. Next table summarizes the results of the experiments:

Benchmark
Node Selected
Time
Orig Size
Slice Size
Reduction (%)
  minmax
13
11 ms.
1.035 bytes
724 bytes
69.95 %
  horseman
45
19 ms.
625 bytes
246 bytes
39.36 %
  lcc
111
33 ms.
784 bytes
613 bytes
78.19 %
  colormap
6
3 ms.
587 bytes
219 bytes
37.31 %
  family_con
6
4 ms.
1453 bytes
262 bytes
18.03 %
  family_nd
0
2 ms.
731 bytes
289 bytes
29.53 %

The meaning of the columns is the following:

Benchmark: It is the name of the benchmark used in the experiment (these benchmarks belong to the Curry's benchmarks collection).
Node Selected : It is the node selected as slicing criterion from the redex trail associated to the execution of the benchmark.
Time: It is the time used for slicing the benchmark measured in milliseconds.
Orig Size: It is the size in bytes of the benchmark.
Slice Size : It is the size in bytes of the slice of the benchmark.
Reduction: It is the code reduction obtained after slicing.


[1] Mark Weiser
Program slicing
Transactions on Software Engineering, 10(4):352-357, 1984.

[2] Frank Tip
A survey of program slicing techniques
Journal of Programming Languages, 3(3):121-189, September 1995.


___________________Download___________________

Program Slicer

DTD