• Aucun résultat trouvé

Some GDC Monolingual Interpreters

Meta-Interpretation

8.6 Some GDC Monolingual Interpreters

Having discussed the background behind the idea of meta-interpreters and given a classification, we can now consider the subject on a practical level by considering a few examples. The following is the vanilla meta-interpreter for GDC:

reduce(X=Y) :- X=Y.

reduce(Actor) :- otherwise

| behavior(Actor, Body), reducelist(Body).

reducelist([]).

reducelist([H|T]) :- reduce(H), reducelist(T).

Since GDC does not have built-in operations for manipulating the behaviors that form a program, the object level behaviors must be represented in a form that explicitly marks them out as object-level behaviors. The following would be the representation for quicksort:

behavior(qsort([],Sorted),Body) :- Body=[Sorted=[]].

behavior(qsort([Pivot|List],Sorted), Body) :- Body=[part(List,Pivot,Lesser,Greater),

qsort(Lesser,Lsorted),qsort(Greater,Gsorted), concatenate(Lsorted,[Pivot|Gsorted],Sorted)].

behavior(part([],Pivot,Lesser,Greater),Body) :- Body=[ Lesser=[],Greater=[]].

behavior(part([Item|List],Pivot,Lesser,Greater),Body) :- Pivot=<Item

| Body=[ Greater=[Item|Upper],part(List,Pivot,Lesser,Upper)].

behavior(part([Item|List],Pivot,Lesser,Greater),Body) :- Item=<Pivot

| Body=[Lesser=[Item|Lower],part(List,Pivot,Lower,Greater)].

behavior(concatenate([],List,Total), Body) :- Body=[Total=List].

behavior(concatenate([Item|List1],List2,Total), Body)

:- Body=[Total=[Item|List],concatenate(List1,List2,List)].

Note that in this interpreter the concurrency of the object level maps implicitly onto the concurrency of the metalevel, the concurrency of the second behavior for reducelist giving the concurrency of the object level. Behavior commitments at the object level map implicitly onto the commitment of the actor in the second behavior of reduce. Object level guards represent metalevel guards. The unification primitive at the object-level maps onto the unification primitive of the first behavior for reduce. Further primitive operations could be covered in a similar way to unification by adding behaviors to reduce.

A number of simple interpreters may be derived from the above meta-interpreter [Safra and Shapiro, 1986]. The following, for example, uses the “short-circuit”

technique to report when execution of an actor has completed:

reduce(X=Y,Left,Right) :- (X,Right)=(Y,Left).

reduce(Actor,Left,Right) :- otherwise

| behavior(Actor,Body), reducelist(Body,Left,Right).

reducelist([],Left,Right) :- Right=Left.

reducelist([H|T],Left,Right) :- reduce(H,Left,Middle),

reducelist(T,Middle,Right).

The initial call to the interpreter will take the form :- reduce(Actor,done,Flag). The message done is sent on the channel Flag only when execution of all actors spawned by the actor execution has completed. Note the assumption in the first behavior for reduce is that the component parts of the unification can be assumed to be done simultaneously (atomic unification). If this is not the case, we need to ensure that the short-circuit is closed only when the unification of the two object level messages has been completed. In this case we will need to use a system primitive which performs unification on its first two arguments and binds a flag channel given as its third argument, the message being sent only when the unification is complete. Assuming the message done is sent on the flag channel following unification, this will give us the following:

reduce(X=Y,Left,Right)

:- unify(X,Y,Flag), close(Flag,Left,Right).

close(done,Left,Right) :- Left=Right.

Similar flag-message sending versions of any other system primitives will be required.

The short-circuit technique can be used to give sequential execution. If we want a pair of actors to execute sequentially, we will not start execution of the second until execution of the first has completely finished. The following meta-interpreter enforces sequential execution:

reduce(X=Y,Flag)

:- (X,Flag)=(Y,done).

reduce(Actor,Flag) :- otherwise

| behavior(Actor, Body), reducelist(Body,Flag).

sequential_reducelist(List,done,Flag) :- reducelist(List,Flag).

reducelist([],Flag) :- Flag=done.

reducelist([H|T],Flag) :- reduce(H,Flag1),

sequential_reducelist(T,Flag1,Flag).

The initial call is :- reduce(Actor,Flag) where Flag is an unbound channel.

In the GDC meta-interpreter above, the selection of a behavior to execute an actor was mapped implicitly onto GDC’s own behavior selection mechanism. If, however, we wish to override the built-in behavior-selection mechanism we must explicitly program in a replacement behavior-selection mechanism. As an example, consider a meta-interpreter where GDC’s indeterminism is resolved by offering a choice between the behaviors, which may reduce an actor. Interpreters like this may be used for debugging purposes letting the human user who is investigating different ways of resolving the indeterminacy make the choice. Alternatively, we could have another layer of meta-interpreter that selects between behaviors, essentially the same idea as the conflict-resolution mechanism in production systems.

The following gives the top level:

reduce(X=Y) :- X=Y.

reduce(Actor) :- otherwise

| behaviors(Actor, Behaviors),

possbodies(Actor, Behaviors, Possibilities), select(Actor, Possibilities, Body),

reducelist(Body).

reducelist([]).

reducelist([H|T]) :- reduce(H),

reducelist(T).

Here behaviors(Actor,Behaviors) is intended to give all behaviors for a possible actor. The actor possbodies(Actor,Behaviors,Possibilities) will return in Possibilities the bodies of all behaviors in Behaviors to which Actor may commit.

The actor select(Actor,Possibilities,Body) will select one of these possibilities.

In this conflict-resolution interpreter, it will be assumed, for convenience, that each behavior in the list of behaviors given by behaviors contains distinct channels and only behaviors with empty guards will be handled. Behaviors are represented by

(Head, Body) pairs. Thus the program for non-deterministic merge would be represented by:

behaviors(merge(X0,Y0,Z0), Behaviors) :- Behaviors=[

(merge(X1,[],Z1),[Z1=X1]),(merge([],Y2,Z2),[Z2=Y2]), (merge([H3|T3],Y3,Z3),[merge(T3,Y3,Z13), Z3=[H3|Z13]]), (merge(X4,[H4|T4],Z4),[merge(X4,T4,Z14), Z4=[H4|Z14]])].

In order to gain a list of possible behavior bodies in possbodies, the OR-parallelism of the object-level needs to be simulated by AND-OR-parallelism at the metalevel as described in Chapter 6. The behavior selection mechanism is made explicit, involving an explicit call to a match actor which performs the matching which is done implicitly in GDC execution. In this case, match as well as receiving messages in the behavior head also sends on a flag channel the message true if the match succeeds and false otherwise:

possbodies(Actor, [(Head,Body)|Rest], Poss) :- match(Actor, Head, Flag),

possbodies(Actor, Rest, RPoss), addposs(Flag, Body, RPoss, Poss).

possbodies(Actor, [], Poss) :- Poss=[].

The actor addposs simply adds a behavior body (with channels appropriately bound) to the list of possibilities if matching succeeds:

addposs(false,_,RPoss,Poss) :- Poss=RPoss.

addposs(true,Body,RPoss,Poss) :- Poss=[Body|Poss].

Although it would be possible to provide a version of match as a system primitive, it may be programmed directly:

match(X, Y, V) :- unknown(Y) | Y=X,V=true.

match(X, Y, V) :- X=Y | V=true.

match(X, Y, V) :- X=/=Y | V=false.

match(X, Y, V) :- list(X), list(Y) | matchlist(X, Y, V).

match(X, Y, V) :- tuple(X), tuple(Y)

| X=..LX, Y=..LY, matchlist(LX, LY, V).

matchlist([], [], V) :- V=true.

matchlist([H1|T1], [H2|T2],V)

:- match(H1, H2, VH), matchlist(T1, T2, VT) and(VH, VT, V).

matchlist(X, Y, V) :- unknown(Y) | Y=X, V=true.

and(true, true, V) :- V=true.

and(false, _, V) :- V=false.

and(_, false, V) :- V=false.

While this meta-interpreter is more complex than previous ones, a large amount of the object-level GDC still maps implicitly onto metalevel GDC. In particular there is no direct reference to the scheduling of actors or the suspension mechanism. The

scheduling of the object-level GDC is whatever is provided by the metalevel GDC. A suspension in the object-level GDC will map into a suspension of the metalevel GDC in the match actor of the meta-interpreter, when X is unbound but Y is bound so the guards X==Y and X=/=Y are both suspended until X becomes sufficiently bound for them to resolve. It is assumed that select will only present a menu of possible bodies to resolve an actor when there are no suspensions for that actor. Note that it is a context free selection as the set of actors awaiting execution remains implicit, so it is not an explicit object that can be viewed when making a behavior selection. The order in which the selection menus are presented will in effect be the scheduling order, which is not under user control.

A further development would be to introduce an explicit scheduling list. This could be used to give a meta-interpreter that implements the actor priorities of Chapter 6.

The following meta-interpreter does this. The idea is that the top-level actor network is:

:- reduce(Actor/Priority,S), scheduler(S)

where S is a stream of messages of the form req(Priority,Go). These messages are generated by the reduce actor and consumed by the scheduler actor. As generated Go is unbound in the messages. The scheduler binds these channels in the order determined by Priority. It is assumed that a behavior body consists of a list of actor/priority pairs. A priority could be a constant or a channel which is bound during execution.

The scheduler actor will keep an explicit list of priority requests, which will in effect form the explicit scheduling list. The wait actor will ensure that behavior selection is suspended until allowed by the scheduler since it cannot proceed until the request channel is bound. Streams of requests are merged using conventional stream merging:

reduce(X=Y, S) :- X=Y, S=[].

reduce(Actor/Priority, S) :- S=[req(Priority,Go)|S1],

behaviors(Actor,Behaviors), wait(Go,Actor,Behaviors,Body), reducelist(Body,S1).

reducelist([],S) :- S=[].

reducelist([H|T],S)

:- reduce(H,S1), reducelist(T,S2), merge(S1,S2,S).

wait(go,Actor,Behaviors,Body)

:- possbodies(Actor,Behaviors,Possibilities), select(Actor,Possibilities,Body).

This interpreter is deficient because there are no limits on the scheduler. The scheduler could just authorize every actor to proceed to behavior selection as soon as it receives the actor’s request. This defeats the purpose of the interpreter, since given a limited number of processors scheduling would default to whichever order is decided by the underlying system. In order to give user-defined scheduling we need to authorize only enough actors to keep the processors busy. To do this, the scheduler

would need to know how many of the current set of actors are suspended on all their possible messages and thus not consuming any processor resources. This could be done with a version of match, but would require an explicit handling of suspensions rather than the implicit mapping of objectlevel suspensions to metalevel suspensions.

Rather than suspend, match would terminate and return a list of channels and messages to which they must be bound for the actor to be woken.

An interpreter that explicitly handled suspensions could either use busy-waiting, continually testing whether a channel whose value is required has become bound, or non-busy-waiting, in which a list of suspended actors is associated with each unbound channel and the actors woken when the channel becomes bound. Such a meta-interpreter would be considerably more complex than the simple meta-meta-interpreters with which we started, but it still relies on implicit mapping from metalevel to object level of store allocation and garbage collection.

Another variant of the meta-interpreter that explicitly gives a selection between behaviors to resolve an actor is one that illustrates the effect of choosing each possible behavior. This is referred to as an all-solutions interpreter since it will give all possible bindings of some channel in an actor. For a more detailed discussion of this problem see [Ueda, 1987]. The way the all-solutions interpreter below works is to maintain a list of actors and a list of partial solutions. When an actor may be resolved in one of several ways a duplicate of these lists, or continuation, is made to represent the position in the computation that would be obtained by choosing each alternative. The continuation will rename each channel (so that, for example, the list [X,X,Y] would become [X1,X1,Y1]), we will assume we have a primitive copy which creates such a copy with renamed channels. The list of possible solutions is obtained by appending together the list of solutions obtained from each continuation.

allsols(Actor,TermSols) :- reduce([Actor],[Term],Sols).

reduce([],Terms,Sols) :- Sols=Terms.

reduce([X=Y|Actors],Terms,Sols) :- X=Y, reduce(Actors,Terms,Sols).

reduce([Actor|Actors],Terms,Sols) :- otherwise

| behaviors(Actor,Behaviors),

reducewith(Actor,Actors,Behaviors,Terms,Sols).

reducewith(Actor,Actors,[(Head,Body)|Rest],Terms,Sols) :- match(Actor,Head,Flag),

reduceon(Flag,Actors,Body,Rest,Terms,Sols1), reducewith(Actor,Actors,Rest,Terms,Sols2), append(Sols1,Sols2,Sols).

reducewith(Actor,Actors,[],Terms,Sols) :- Sols=[].

reduceon(true,Actors,Body,Rest,Terms,Sols) :- append(Body,Actors,Actors1),

copy([Terms,Actors1],[Terms2,Actors2]), reduce(Actors2,Terms2,Sols).

reduceon(false,Actors,Body,Rest,Terms,Sols) :- Sols=[].

The effect of a call allsols(Actor,Term,Sols) is to bind Sols to a list of the different instances of Term given by all possible evaluations of Actor. For example, a call allsols(merge([a,b],[c],X),X,Sols) will cause Sols to become bound to [[a,b,c],[a,c,b],[c,a,b]], assuming we have the representation of non-deterministic merge given previously.

In the interpreter reducewith matches an actor against the heads of each of the behaviors for that actor. If matching succeeds, reduceon sets up a reduce actor to construct all solutions which involve this particular matching. Note that the code interpreted by this interpreter is limited to examples which do not require any suspensions since reduceon which initiates solution of the remaining actors remains suspended until matching has completed and sent the message true or false on the channel Flag. Since matching does not bind channels in the actor, it is safe to leave the construction of a continuation until after matching has completed successfully, thus avoiding unnecessary copying for cases where matching fails.