| % |
| % This code implements a simple CTR interpreter. |
| % It assumes that backtrackable updates are provided. |
| % |
:- import append/3 from basics.
| % USER INTERFACE: To execute a goal, the user types exec(Goal), |
exec(Goal) :- execute(all,Goal).
| % THE INTERPRETER: The predicate execute(Flag,Goal) executes Goal by |
| % recursively executing hot components one at a time until the empty |
| % goal (state) is reached. |
| % |
| % The variable Flag affects concurrency. Given a set of concurrent |
| % processes, Flag specifies how many different interleavings of these |
| % processes should be checked. The value of Flag (and thus the |
| % treatment of concurrency) is fixed within the scope of each |
| % isolation operator. At present, regardless of the value of Flag, |
| % the first interleaving to be checked is always based on round-robin |
| % scheduling. |
| % |
| % At present, Flag can take two values, "all" and "one." When Flag=all, |
| % the interpreter checks all possible interleavings of the processes until |
| % one is found that succeeds. When Flag=one, the interpreter checks |
| % only one interleaving. This is useful for commutative transactions, |
| % where all interleavings give the same result. In this case, if one |
| % interleaving fails, then they all fail, so there is no point of |
| % checking more than one interleaving. The value of flag is initially |
| % "all" (see above), and can be changed by each isolation operator (see below). |
| execute(Flag,Goal1) :- not(Goal1 = state), |
| oneStep(Flag,Goal1,Goal2), |
| execute(Flag,Goal2). |
execute(Flag,state).
| % Execute one hot component of Goal1. In the process, Goal1 is |
| % transformed into Goal2, which is then simplified to Goal3. |
| oneStep(Flag,Goal1,Goal3) :- doOne(Flag,Goal1,Goal2), |
| simplify(Goal2,Goal3). |
| % ISOLATION: An isolated goal is a hot component; so, to execute one |
| % step, the entire goal is executed, and the empty goal is returned. |
| % |
| % There are two modalities of isolation, isolate(G) and isolate1(G), |
| % which affect the interleaving of concurrent processes in G. |
| % isolate(G) causes all interleavings to be checked, while isolate1(G) |
| % causes only one interleaving to be checked. |
doOne(Flag,isolate(G),state) :- !, execute(all,G).
doOne(Flag,isolate1(G),state) :- !, execute(one,G).
| % SEQUENTIAL COMPOSITION: Execute one step of a sequential goal. The |
| % value of Flag is irrelevant. |
| doOne(Flag,seq([]),state) :- !. |
| % Special case. Do nothing, but simplify the goal. |
doOne(Flag,seq([G1|Goals]),seq([G2|Goals])) :- !, oneStep(Flag,G1,G2).
| % DISJUNCTIVE GOALS: A disjunctive goal is a list of processes. To |
| % execute the goal, we must execute one of its disjuncts, chosen |
| % non-deterministically. The rules below first choose a disjunct, |
| % then execute one step, and then return the rest. (More precisely, |
| % the rules choose the first disjunct, and on backtracking, they |
| % choose other disjuncts.) The value of Flag is irrelevant. |
| doOne(Flag,or([]),G) :- !, fail. % No disjuncts. Fail. |
| doOne(Flag,or([G1]),G2) :- !, oneStep(Flag,G1,G2). |
| % Only 1 disjunct to choose from. |
| doOne(Flag,or([G1|Goals]),G2) :- oneStep(Flag,G1,G2). |
| % Many disjuncts: choose the first one. |
| doOne(Flag,or([G1|Goals]),G2) :- !, doOne(Flag,or(Goals),G2). |
| % Choose another one. |
| % conc1 is a form of concurrent conjunction for transactions that |
| % commute (ie, for transactions for which the order of execution does |
| % not matter). In this case, the interpreter tries only one interleaving. |
| % |
| doOne(Flag,conc1(Goals1),conc1(Goals2)) :- !, |
| doOne(one,conc(Goals1),conc(Goals2)). |
| % CONCURRENT COMPOSITION: A concurrent goal is a list of processes. |
| % To execute one step of the goal, we execute one step of some process |
| % in the list. After execution, we rotate the list by 1 position, which |
| % assures round-robin scheduling in the absense of backtracking. |
| doOne(Flag,conc([]),state) :- !. |
| % Special case. Do nothing, but simplify the goal. |
| % If Flag=one, then we always choose the first process in the goal list. |
| doOne(one,conc(Goals1),conc(Goals4)) :- !, |
| [Ga|Goals2] = Goals1, % Extract the first process. |
| oneStep(one,Ga,Gb), % Execute one step of the process. |
| Goals3 = [Gb|Goals2], % Put first process back into the list. |
| rotate1(Goals3,Goals4). % Rotate list by 1 position. |
| % Rotate a list by 1 position. If the first list element is state, |
| % then simply chop it off. |
| rotate1([state|Goals],Goals) :- !. |
| rotate1([G|Goals1],Goals2) :- !, append(Goals1,[G],Goals2). |
| % If Flag=all, then on backtracking, we will eventually choose every |
| % process in the goal list. (This guarantees that all interleavings of |
| % the processes are eventually generated.) To choose a process, we |
| % rotate the list by some amount, and select the first process in the |
| % rotated list. More precisely, we initially rotate the list by 0 |
| % positions; then, on backtracking, we rotate it by 1 position, 2 |
| % positions, 3 positions, etc. In this way, every process is eventually |
| % rotated to the front of the list. Moreover, in the absense of |
| % backtracking, the first process in the list is always chosen. |
| doOne(all,conc(Goals1),conc(Goals5)) :- !, |
| rotate(Goals1,Goals2), % Rotate the list of processes. |
| [Ga|Goals3] = Goals2, % Extract the first process. |
| oneStep(all,Ga,Gb), % Execute one step of the process. |
| Goals4 = [Gb|Goals3], % Put first process back into the list. |
| rotate1(Goals4,Goals5). % Rotate list by 1 position. |
| % Rotate list L1 by some amount, to give list L2. |
| rotate(L1,L2) :- split(L1,L1a,L1b), |
| not(L1b = []), |
| append(L1b,L1a,L2). |
| % Split list L into two sublists, L1 and L2. |
| split(L,L1,L2) :- append(L1,L2,L). |
| % TRANSACTION BASE: Search the transaction base for rules of the form |
| % Head<-Body. If such a rule is found, then, to execute one step of |
| % "Head", we execute one step of "Body". This transforms "Body" into |
| % "Goal". Note that the CTR rule Head<-Body is implemented as the |
| % Prolog rule trans(Head):-Body. The value of Flag is irrelevant. |
| doOne(Flag,Head,Goal) :- clause(trans(Head),Body), |
| oneStep(Flag,Body,Goal). |
| % doOne(Flag,Head,Goal) :- clause(trans(Head),Body), !, fail. |
| % STATE: An empty goal does nothing and always succeeds. |
| % |
| doOne(Flag,state,state) :- !. |
| % PROLOG COMMANDS in CTR formulas: If the interpreter gets this far, |
| % then assume that P is a Prolog command and execute it, leaving the |
| % empty goal (state). |
| doOne(Flag,P,state) :- !, P. |
| % SIMPLIFICATION: Here are some basic rules for simplifying a goal |
| % after a single step has been executed. Intuitively, simplify(G1,G2) |
| % means that goal G2 is equivalent to but syntactically simpler than |
| % goal G1. |
| simplify(seq([]),state) :- !. |
| simplify(seq([state]),state) :- !. |
| simplify(seq([state|Goals]),seq(Goals)) :- !. |
| simplify(conc([]),state) :- !. |
| simplify(conc([state]),state) :- !. |
| simplify(conc([state|Goals]),conc(Goals)) :- !. |
| simplify(conc1([]),state) :- !. |
| simplify(conc1([state]),state) :- !. |
| simplify(conc1([state|Goals]),conc1(Goals)) :- !. |
| simplify(G,G) :- !. |
| % MONITORING EXECUTION: The command monitor(Task) helps a user |
| % to monitor the execution of the various tasks that make up a CTR program. |
| % It prints a message when a task is committed, and then prints another |
| % message if the task is later rolled back and undone. Specifically, |
| % when monitor(Task) is executed, it prints "committing Task" on the |
| % screen. During backtracking, it prints "Undoing Task". |
| monitor(Task) :- write('committing '), write(Task), nl, !, |
| onBacktracking(Task). |
| onBacktracking(Task). |
| onBacktracking(Task) :- write('undoing '), |
| write(Task), nl, !, fail. |