• Aucun résultat trouvé

An Algorithm for Partial Evaluation of GDC Programs

Partial Evaluation

9.6 An Algorithm for Partial Evaluation of GDC Programs

The first stage of partial evaluation would be to execute the program under normal GDC evaluation. This leads to the point where a computation will consist entirely of actors suspended due to insufficient variable binding and may be considered a form of unfolding. GDC evaluation suspends an actor if expansion depends on the value of some variable that at the time of evaluation is undefined. This is similar to Ershov’s [1980] mixed computation suspending a computation and adding it to the residual program if it depends on information that is dynamic.

In partial evaluation, rather than just suspend the actors we make specialized programs for them. For example, if we had an actor :- p(a,b,c,4,A,B) and behaviors:

p(a,V,W,X,Y,Z) :- q(V,X,M,N), r(M,Y,P), s(W,N,P,Z).

q(b,A,B,C) :- f(A,B,C).

r(c,Y,Z) :- g(Y,Z).

r(d,Y,Z) :- h(Y,Z).

s(W,X,Y,Z) :- X>5 | t(W, Z).

s(W,X,Y,Z) :- X<5 | u(X,Y,Z).

f(I,J,K) :- I>3 | m(J,K).

f(I,J,K) :- I=<3 | n(J,K).

the actor can be reduced to the point where we have the subactors :-m(M,N),r(M,A,P),s(c,N,P,B), but no further since none of the actors at this point is sufficiently bound to allow further reduction. However, the actor s(c,N,P,B) has its first variable bound, so we can specialize it to some new actor sc(N,P,B) with a program giving the behavior of s when restricted to the cases where its first argument is c.

One distinction between unfolding and normal evaluation in event driven languages is that noted by Furukawa et al. [1988], who developed a set of rules for unfolding programs in the concurrent logic language GHC, that no assignment to variables in the initial actor should take place. This is because in these languages, for example, a clause p(X,Y) :- X=a, q(X,Y) will cause the actor p(U,V) when executed to send a message a to the channel U, q(X,Y) will be executed simultaneously, not necessarily waiting for its first argument to be bound to a unless this binding is needed for it to proceed further) whereas a clause p(a,Y) :- q(a,Y) will cause the actor p(U,V) to be suspended waiting for message a to arrive at channel U so that it can match. During unfolding, any assignment to a variable that occurred in the initial actor is left suspended since otherwise the semantics of the program would be changed, but the right-hand side of the assignment replaces occurrences of that variable in other actors being unfolded. Another way of thinking of it is to ensure that output assignments occur during final evaluation rather than prematurely during partial evaluation.

The unfolding mechanism described above means that non-deterministic choices made in commitment in event driven languages are made during partial evaluation

when possible. This means that the residual program will not be equivalent to the initial program in terms of the solutions which it is possible it may give, since some may have been lost during partial evaluation. If this is undesirable, it may be avoided by restricting the unfolding to actors where there is only one possible commitment, suspending for the next stage of partial evaluation those actors where an indeterministic choice is possible.

The next stage is the specialization of individual suspended actors, referred to as reparametrization in Sahlin’s [1993] work on partial evaluation of Prolog. It means constructing a set of behaviors specialized to the particular bindings in an actor. The process of specialization can be broken down to the following steps:

1. Remove any behaviors with which the actor cannot match.

2. Pass through any bindings in the actor to the remaining behaviors.

3. Rename the actor, restrict its argument to the actor’s variables.

4. Partially evaluate the bodies of the behaviors.

Stage 4 is a recursive call on the entire partial evaluation algorithm, except that when we partially evaluate a set of actors, message sends to channels which occur in the head of the behavior are not executed for the reasons noted. The whole process is similar to the specialization process described diagrammatically above in supercompilation. The removal of behaviors which cannot match in Stage 1 is equivalent to the removal of arcs with assignments of a variable to one value and a condition requiring it to have another. Stage 2 is the pushing down of the messages, or constant propagation. Note that when the messages are passed through, conditions in the guard of a behavior may become further bound, a guard condition being removed if it evaluates true, the whole behavior being removed if it evaluates false.

The diagrammatic notation of supercompilation did not directly name the nodes, but the renaming of stage 3 is equivalent to the establishment of a separate node.

As an example of specialization, consider the initial actor: g([A,3|B],C,4,D) with behaviors:

g([W],X,Y,Z) :- a(W,X,Y,Z).

g([H|T],6,Y,Z) :- b(H,T,Y,Z).

g([U,V|W],X,Y,Z) :- V>5 | c(U,W,X,Y,Z).

g([U,V|W],7,Y,Z) :- V=<5 | d(U,W,Y,A,B), e(A,B,X,Z).

g([H|T],X,Y,Z) :- X>=8 | f(H,Y,U,V), g([U|T],V,Y,Z).

The steps in specialization are as follows:

1. Behavior removal

first behavior removed g([H|T],6,Y,Z) :- b(H,T,Y Z).

g([U,V|W],X,Y,Z) :- V>5 | c(U,W,X,Y,Z).

g([U,V|W],7,Y,Z) :- V=<5 | d(U,W,Y,A,B), e(A,B,X,Z).

g([H|T],X,Y,Z) :- X>=8 | f(H,Y,U,V), g([U|T],V,Y,Z).

2. Passing through messages g([H,3|B],6,4,Z) :- b(H,[3|B],4,Z).

g([U,3|W],X,4,Z) :- 3>5 | c(U,W,X,4,Z).

guard becomes false g([U,3|W],7,4,Z) :- 3=<5 | d(U,W,4,A,B), e(A,B,X,Z).

guard becomes true g([H,3|B],X,4,Z) :- X>=8 | f(H,4,U,V), g([U,3|B],V,4,Z).

3. Rename predicate giving actor g1(A, B, C, D) g1(H,B,6,Z) :- b(H,[3|B],4,Z).

g1(U,W,7,Z) :- d(U,W,4,A,B), e(A,B,X,Z).

g1(H,B,X Z) :- X>=8 | f(H,4,U,V), g([U,3|B],V,4,Z).

4. Partially evaluating behavior bodies g1(H,B,6,Z) :- b1(H,B,Z).

g1(U,W,7,Z) :- d1(U,W,A,B), e(A,B,X,Z).

g1(H,B,X Z) :- X>8 | f1(H,U,V), g2(U,B,V,Z).

At Stage 4 in the above example, it was assumed that during the specialization of the actor g([A,3|B],C,4,D) to g1(A,B,C,D) the actor g([U,3|B],V,4,Z) would be found in the body of the third behavior and the algorithm applied recursively giving some new actor g2(U,B,V,Z). However g([U,3|B],V,4,Z) is identical to g([A,3|B],C,4,D) in all but variable name. If specialization were to proceed it would do so identically as above except for variable names, resulting in the need to specialize g([U´,3|B],V´,4,Z´) and so on infinitely.

Clearly it is possible to specialize g([U,3|B],V,4,Z) to a recursive call g1(U,B,V,Z).

This is a case where we are looking for equivalences, in our diagrammatic notation making a loop which turns an infinite tree into a graph. In this case, the equivalence is simple, as the two calls are identical in all but variable name. However, the stop criterion “equivalent except for variable names” is too weak and will not prevent infinite unfolding in all cases. Consider specialization of a program that performs list reversing with an accumulator and also applies a function with argument n to items in the list.

The original program is:

freverse(N,[],Acc,R) :- R=Acc.

freverse(N,[H|T],Acc,R)

:- f(N,H,X), freverse(N,T,[X|Acc] R).

If we specialize freverse(n,A,[] B) with these behaviors we get, before constant propagation:

freverse1([],R) :- R=[].

freverse1([H|T],R) :- f(n,H,X), freverse(n,T,[X],R).

The actor f(n,H,X) will specialize to some actor fn(H,X). The second actor here requires specialization and is not equivalent in all but channel names to the previous

actorl freverse(n,A,[],B), so we can specialize freverse(n,T,[X],R) to freverse2(T,X,R) with behaviors:

freverse2([],X,R) :- R=[X].

freverse2([H|T],X,R) :- f(n,H,X1), freverse(T,[X1,X],R).

The actor f(n,H,X1) specializes to fn(H,X1) but freverse(T,[X1,X], R) is not identical in all but variable names to a previously specialized actor so it becomes freverse3(T,X1,X,R) and so on. We end up constructing a separate actor for every possible length of the accumulator and specialization continues without terminating.

What is needed is a recognition that freverse(n,A,[],B) and freverse(n,T,[X],R) are related. In the place of separate specializations, we specialize freverse(n,A,Acc,B) to freverse0(A,Acc,B), replacing freverse(n,A,[],B) by freverse0(A,[],B) and freverse(n,T,[X],R) by freverse0(T,[X],R). That is, we are generalizing by abstracting out an argument that is diverging on recursive calls. This is a formalization of Turchin’s generalization in his supercompilation.

In general, if specializing an actor aB and we have previously specialized an actor of the same predicate name and arity aA, we check for recursion by matching the arguments of aA with those of aB. A divergence is found where a constant in aA matches against a variable or tuple in aB, two differing constants match, or where two tuples A and B of differing name or arity match, such that there is no well-founded ordering f such that AfB. Bruynooghe et al. [1992] have given a detailed consideration of the use of well-founded orderings for avoiding infinite specialization in partial evaluation. Consideration of suitable well-founded orderings for actor languages is currently a matter under investigation.

We need to find an actor which generalizes aA and aB, that is an actor a such that there are substitutions θ and ψ where aψ=aA and aθ=aB. The actor should be the least general generalization, that is such that there is no ψ´, θ´ and a´ where a´ψ´=aA and a´θ´=aB and |ψ´|<|ψ|. Plotkin [1970] first described this process under the name anti-unification.

Following generalization we specialize a to give a´, return a´θ as the residual actor for aB and replace the original specialized version of aA by a´ψ. It should be recalled that the specialization of aB and hence generalization may occur within the attempt to construct behaviors specialized for aA, if this is the case the specialization of aA, referred to as “overspecialization”, is abandoned. This is referred to as generalized restart by Sahlin [1993].

In the special case where ψ is empty, we can use the behaviors originally obtained from the specialization of aA and replace by a recursive call to the actor introduced then (i.e. aAθ). This covers cases where no generalization is required and actors are equivalent except for variable names.

An algorithm that may be used for generalization is given in Figure 9.6.1.

set θ, ψ and ξ to {}; A to the list of arguments in aA

set B to the list of arguments in aB; C to the list of arguments in g0

repeat

let hA = head(A), hB = head(B), hC = head(C), let tA = tail(A), tB = tail(B), tC = tail(C)

if hB is a variable then if hA is a variable then

set θ to θ∪{hA:=hB}, ξ to ξ∪{hC:=hA}, A to tA, B to tB, C to tC else set ψ to ψ∪{hB:=hA}, ξ to ξ∪{hC:=hB}, A to tA, B to tB, C to tC else if hA is a variable then

if hB a tuple or hA is a result of a previous generalization then set ξ to ξ∪{hC:=hA}, θ to θ∪{hC:=hB}, A to tA, B to tB, C to tC else exit, convergence found

else if hA and hB are tuples then

if the names and arities of hB and hA are the same then let M be the most general term of hB in

set A to the arguments of hA appended to tA set B to the arguments of hB appended to tB set C to the arguments of M appended to tC set ξ to ξ∪{hC:=M}

else if hAf hB for some f then exit, convergence found

else set θ to θ∪{hC:=hB}, ψ to ψ∪{hC:=hA}, A to tA, B to tB, C to tC else if hA is a tuple then exit, convergence found

else if hA=hB then set ξ to ξ∪{hC:=hB}, A to tA, B to tB, C to tC else

set θ to θ∪{hC:=hB}, ψ to ψ∪{hC:=hA}, A to tA, B to tB, C to tC until A is empty

Figure 9.6.1 Generalization algorithm

On exit from this algorithm, the generalization a is given by a0ξ where a0 is the most general actor for aA and aB that is, an actor of the same name and arity but with each argument a new distinct variable. If the algorithm exits early with convergence, then it is shown that no looping is occurring, aA is left alone and aB specialized to a new actor. It is necessary to check that this convergence is not in fact a reintroduction of an over-specialization, so if a variable matches against a non-variable, it is only treated as a convergence if the variable did not arise from a previous generalization.

Unless a definite convergence is found, the algorithm will always convert aA and aB to calls on instances of an actor for a0ξ, or convert the call aB to a recursive call on

the actor for aA when ψ is empty. In the latter case, no more partial evaluation is done. In the former case, since a0ξ is a generalization and since it is not possible to infinitely generalize (the most general actor will eventually be reached), partial evaluation will always terminate.

Sahlin [1993] uses for his version of our test of convergence hAf hB the test termsize(hA) > termsize(hB), where termsize(x) is defined on Prolog terms as:

termsize(x) =

This generalization algorithm, however, is not sufficient to partially evaluate many meta-interpreters. Consider the case where we have

reduce(Actor) :- rule(Actor,Body), reduce(Body) rule(f(X),Body) :- Body:=g(X).

Partial evaluation of reduce(f(X)), to give a new actor reducef(X) with appropriate behaviors, will come across the partial evaluation of reduce(g(X)) and since it is not the case that termsize(f(X))>termsize(g(X)), both generalize back to the original reduce(f(X)) and reduce(g(X)). So we fail to evaluate away the interpreter.

In fact, two tuples with differing names or arities are only possibly diverging if we have an infinite number of names. If we cannot generate any new tuple names, that is there is no use of the Prolog univ (=..) or its equivalents which can convert from a list to a tuple whose functor is the item at the head of a list and whose arguments are the items on its tail, we can safely take a matching of tuples with differing names or arities as a convergence, since we cannot have an infinite chain of actors each differing from the previous ones in matched tuple name.

Going further, if it is not possible to generate new constants (that is there is no use of list to string primitives) we can safely assume a convergence has been found if two actors differ only in matching constants and it follows that if we cannot generate any new names at all, we cannot generate any new tuple names.