% |
% 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. |