Programming with angelic nondeterminism - Computer Science

13
Programming with Angelic Nondeterminism Shaon Barman Rastislav Bodík Satish Chandra Joel Galenson Doug Kimelman Casey Rodarmor Nicholas Tung University of California, Berkeley IBM T.J. Watson Research Center Abstract Angelic nondeterminism can play an important role in program de- velopment. It simplifies specifications, for example in deriving pro- grams with a refinement calculus; it is the formal basis of regular expressions; and Floyd relied on it to concisely express backtrack- ing algorithms such as N-queens. We show that angelic nondeterminism is also useful during the development of deterministic programs. The semantics of our angelic operator are the same as Floyd’s but we use it as a substitute for yet-to-be-written deterministic code; the final program is fully deterministic. The angelic operator divines a value that makes the program meet its specification, if possible. Because the operator is executable, it allows the programmer to test incomplete programs: if a program has no safe execution, it is already incorrect; if a program does have a safe execution, the execution may reveal an implementation strategy to the programmer. We introduce refinement-based angelic programming, describe our embedding of angelic operators into Scala, report on our imple- mentation with bounded model checking, and describe our experi- ence with two case studies. In one of the studies, we use angelic op- erators to modularize the Deutsch-Schorr-Waite (DSW) algorithm. The modularization is performed with the notion of a parasitic stack, whose incomplete specification was instantiated for DSW with angelic nondeterminism. Categories and Subject Descriptors D.2.1 [Requirements/Spec- ifications]: Methodologies; D.2.2 [Tools and Techniques]: Top- down programming; D.2.4 [Software/Program Verification]: Vali- dation General Terms Design, Languages, Verification Keywords Angelic non-determinism, constraints, bounded model- checking, traces, refinement 1. Introduction Model checking and testing leverage compute power to validate complete programs. However, much less work has addressed the issue of how to assist with the construction of a program. This paper proposes using the clairvoyance of angelic nondetermin- Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. POPL’10, January 17–23, 2010, Madrid, Spain. Copyright c 2010 ACM 978-1-60558-479-9/10/01. . . $10.00. ism in the solving of programming problems. The programmer encodes his partial understanding of the programming problem in an angelic program, relying on the (executable) nondetermin- istic choose operator to produce values that the programmer is as yet unable to compute. The system answers whether the angelic program has a safe execution for a given input; if so, the sys- tem outputs the program’s traces. The process of programming with angelic nondeterminism amounts to (1) testing hypotheses about plausible solutions by formulating angelic programs and, as the understanding develops, (2) gradually refining the angelic program until all angelic nondeterminism is removed. An angelic program is a program with a choose operator. This nondeterministic operator was first proposed by Floyd [7], who intended it as programming abstraction for hiding implementa- tion details of backtracking search, with the goal of allowing ab- stract specifications of algorithms such as N-queens. The choose operator divines a value that makes the program terminate with- out violating an assertion, if possible. We say that an angelic pro- gram is correct if such a safe execution exists for all inputs. The operator is nondeterministic because it can evaluate to any suit- able value; it is angelic because it collaborates with the program against the environment (i.e., the input). The input sequence can be seen as demonic nondeterminism, finding a value that might make the program fail. The choose operator is clairvoyant in that it looks ahead into the execution and returns a value that allows further choose operators to return values that lead to a successful execution, if possible. Angelic nondeterminism can be a deduc- tive device (e.g., [4]) or it can be executable [7], with implemen- tations relying on backtracking or constraint solving (Section 7). Our choose operator is executable but, unlike Floyd, we pro- pose to use it only during program development. It is used to ex- press partially-developed programs; final programs are choose- free. The choose operator stands for code that the programmer has not yet implemented, either because he does not yet know how to implement it or because he does not even know what values it should produce. The choose operator produces values based on a correctness condition; the final program will compute these values with deterministic code fragments. Executable angelic nondeterminism aids programming in several ways. First, the programmer can test hypotheses about his implementation strategies. He does so by formulating an an- gelic program, using the choose operators as substitutes for as yet unimplemented code fragments. An example of a hypothe- sis that the programmer may test is whether there exists a way to satisfy a postcondition in at most n iterations of a loop; the loop will typically have angelic statements in the body. If no safe angelic execution exists, the hypothesis is rejected. The op- erator represents the most general way of computing a subtask, 339

Transcript of Programming with angelic nondeterminism - Computer Science

Programming with Angelic Nondeterminism

Shaon Barman† Rastislav Bodík† Satish Chandra! Joel Galenson†Doug Kimelman! Casey Rodarmor† Nicholas Tung†

†University of California, Berkeley !IBM T.J. Watson Research Center

AbstractAngelic nondeterminism can play an important role in program de-velopment. It simplifies specifications, for example in deriving pro-grams with a refinement calculus; it is the formal basis of regularexpressions; and Floyd relied on it to concisely express backtrack-ing algorithms such as N-queens.

We show that angelic nondeterminism is also useful duringthe development of deterministic programs. The semantics of ourangelic operator are the same as Floyd’s but we use it as a substitutefor yet-to-be-written deterministic code; the final program is fullydeterministic. The angelic operator divines a value that makes theprogram meet its specification, if possible. Because the operator isexecutable, it allows the programmer to test incomplete programs:if a program has no safe execution, it is already incorrect; if aprogram does have a safe execution, the execution may reveal animplementation strategy to the programmer.

We introduce refinement-based angelic programming, describeour embedding of angelic operators into Scala, report on our imple-mentation with bounded model checking, and describe our experi-ence with two case studies. In one of the studies, we use angelic op-erators to modularize the Deutsch-Schorr-Waite (DSW) algorithm.The modularization is performed with the notion of a parasiticstack, whose incomplete specification was instantiated for DSWwith angelic nondeterminism.

Categories and Subject Descriptors D.2.1 [Requirements/Spec-ifications]: Methodologies; D.2.2 [Tools and Techniques]: Top-down programming; D.2.4 [Software/Program Verification]: Vali-dation

General Terms Design, Languages, Verification

Keywords Angelic non-determinism, constraints, bounded model-checking, traces, refinement

1. IntroductionModel checking and testing leverage compute power to validatecomplete programs. However, much less work has addressed theissue of how to assist with the construction of a program. Thispaper proposes using the clairvoyance of angelic nondetermin-

Permission to make digital or hard copies of all or part of this work for personal orclassroom use is granted without fee provided that copies are not made or distributedfor profit or commercial advantage and that copies bear this notice and the full citationon the first page. To copy otherwise, to republish, to post on servers or to redistributeto lists, requires prior specific permission and/or a fee.POPL’10, January 17–23, 2010, Madrid, Spain.Copyright c© 2010 ACM 978-1-60558-479-9/10/01. . . $10.00.

ism in the solving of programming problems. The programmerencodes his partial understanding of the programming problemin an angelic program, relying on the (executable) nondetermin-istic choose operator to produce values that the programmer is asyet unable to compute. The system answers whether the angelicprogram has a safe execution for a given input; if so, the sys-tem outputs the program’s traces. The process of programmingwith angelic nondeterminism amounts to (1) testing hypothesesabout plausible solutions by formulating angelic programs and,as the understanding develops, (2) gradually refining the angelicprogram until all angelic nondeterminism is removed.

An angelic program is a program with a choose operator. Thisnondeterministic operator was first proposed by Floyd [7], whointended it as programming abstraction for hiding implementa-tion details of backtracking search, with the goal of allowing ab-stract specifications of algorithms such as N-queens. The chooseoperator divines a value that makes the program terminate with-out violating an assertion, if possible. We say that an angelic pro-gram is correct if such a safe execution exists for all inputs. Theoperator is nondeterministic because it can evaluate to any suit-able value; it is angelic because it collaborates with the programagainst the environment (i.e., the input). The input sequence canbe seen as demonic nondeterminism, finding a value that mightmake the program fail. The choose operator is clairvoyant in thatit looks ahead into the execution and returns a value that allowsfurther choose operators to return values that lead to a successfulexecution, if possible. Angelic nondeterminism can be a deduc-tive device (e.g., [4]) or it can be executable [7], with implemen-tations relying on backtracking or constraint solving (Section 7).

Our choose operator is executable but, unlike Floyd, we pro-pose to use it only during program development. It is used to ex-press partially-developed programs; final programs are choose-free. The choose operator stands for code that the programmerhas not yet implemented, either because he does not yet knowhow to implement it or because he does not even know whatvalues it should produce. The choose operator produces valuesbased on a correctness condition; the final program will computethese values with deterministic code fragments.

Executable angelic nondeterminism aids programming inseveral ways. First, the programmer can test hypotheses abouthis implementation strategies. He does so by formulating an an-gelic program, using the choose operators as substitutes for asyet unimplemented code fragments. An example of a hypothe-sis that the programmer may test is whether there exists a wayto satisfy a postcondition in at most n iterations of a loop; theloop will typically have angelic statements in the body. If nosafe angelic execution exists, the hypothesis is rejected. The op-erator represents the most general way of computing a subtask,

339

but the programmer can declaratively (with assertions) constrainit in ways that reflect his hypothesis. For example, the operatormay be prevented from visiting a graph node multiple times. Theenvisioned benefit to the programmer is that the implementationstrategy can be safely aborted before the programmer understoodits infeasibility on his own.

Second, if the angelic program is correct, the system outputsits safe traces. The traces serve as demonstrations of how to per-form the desired task, for example: how to rotate nodes to es-tablish the red-black property. Given these demonstrations, theprogrammer is left with the hopefully simpler task of generaliz-ing the traces into an algorithm that works for all inputs. Thesedemonstrations represent vast amounts of combinatorial reason-ing that the programmer may otherwise have to carry out on hisown. The envisioned benefit of angelic demonstrations is a rev-elation of an insight into how to carry out a computation undergiven time or space constraints, potentially leading to faster dis-covery of the algorithm.

An angelic program may, of course, generate many alterna-tive traces, and most of the traces may be the result of chooseoperators “abusing” their clairvoyance to complete the computa-tion in an input-dependent fashion that does not correspond to aneasily encodable deterministic algorithm. The programmer canexclude such irregular traces with assertions that limit nondeter-ministic choices. For example, the operator may be required tosatisfy a data structure consistency invariant or to evaluate onlyto values that have already been stored in memory.

Third, nondeterminism allows refinement-based program-ming. The programmer can develop the program gradually inseveral ways. As mentioned above, by adding assertions, he canprune the set of safe traces, working towards a trace that corre-sponds to a desired algorithm. The programmer can also focus onone subproblem at a time, ignoring those subproblems expressedwith choose. In general, it is not possible to refine angelic opera-tors independently of each other, but as we show in Section 5.2,sometimes we can add sufficient constraints in the program forthem to be independent. The programmer can also implement thechoose operator with a subprogram that is itself nondeterminis-tic, applying the methodology recursively until a deterministicprogram is obtained.

Finally, the clairvoyance of the choose operator helps theprogrammer avoid complex global reasoning. In Section 8, werely on this property to modularize the Deutsch-Schorr-Waite(DSW) algorithm [15]. We refactor the algorithm to hide itsbacktracking logic in a parasitic stack, a data structure that storesits values in memory locations borrowed from the host datastructure, i.e., the graph being traversed. The hard decisions ofwhich memory locations the parasitic stack is allowed to borrowand how to restore their values are left to choose operators.The programmer has to solve only the more local problem ofhow to implement the stack with these borrowed locations. Infact, without the choose, we were unable to carry out this newmodularization of DSW.

This paper makes these contributions:

• We propose a program development methodology based onrefinement of angelic programs. In contrast to previous workin program refinement, our methodology permits the exe-cution of incomplete programs, which helps a programmerin several ways, from rejecting infeasible implementationstrategies to making reasoning more local.

• We add the choose construct to the Scala programming lan-guage [13]. We describe our implementation of angelic non-

determinism. We also present experimental results on the ef-ficiency of two different implementation strategies, one basedon backtracking and one on SAT solving.

• We present case studies of two classic problems: the DutchFlag problem [6, 10] and the Deutsch-Schorr-Waite graphmarking algorithm [15]. For the second problem, angelic pro-gramming allowed us to develop a new way of modularizingthe algorithm that we believe is novel. We also include a casestudy on recursive list manipulation.

Section 2 gives an overview of the programming methodol-ogy using the case study of the Dutch Flag problem. Sections 3–5formalize the methodology and give refinement transformations.Section 6 compares our (algorithmic) methodology with the (de-ductive) refinement of Morgan. Section 7 describes our imple-mentation and Section 8 reports on the case study of the DSWalgorithm. Related work is described in Section 9.

2. OverviewThis section gives an overview of programming with angelicnondeterminism on the Dutch Flag problem [6]. The section doesnot give a beautified tutorial example; instead, we present ratherfaithfully how a programmer used the programming methodol-ogy. We highlight three aspects of angelic programming:• How an angelic program can express a hypothesis about the

programmer’s implementation plan, which will be rejected ifit is guaranteed not to lead to a correct implementation.

• How the programmer improves his understanding of theproblem by observing angelic executions.

• How angelic programs are refined by constraining the angelsand by deterministically implementing them.

Dijkstra presents the Dutch Flag problem in [6]: given anarray of n pebbles, each of which is either red, white, or blue,the algorithm must sort them in-place, in order of the colors inthe Dutch Flag: first red, then white, then blue. The algorithmmust examine the color of each pebble at most once and can onlymove the pebbles by swapping. A crucial constraint is that onlya constant amount of storage can be used to remember the colorof pebbles that have been examined.

In traditional algorithm development, a programmer mustimplement the entire algorithm first before he can test it. Perhapshe can proceed bottom up, testing small subroutines first. Withangelic nondeterminism, one can proceed top down, starting witha very nondeterministic program that implements coarse stepsof the entire algorithm. In the case of the Dutch Flag problem,the programmer starts with this initial angelic program, whichtests the hypothesis that the problem can indeed be solved with asequence of swap operations.

Program P0

while ( choose ) {swap(choose(n), choose(n))

}assert isCorrect

The choose operator nondeterministically chooses a Booleanvalue, and the expression choose(n) nondeterministically choosesa value from the range [0, n).

This angelic program is correct (i.e. the angels can find a safeexecution that validates the hypothesis).1 In thinking about how

1 Our tester validates the hypothesis on only a small set of inputs, so thebest we can actually ascertain is that the hypothesis has not been rejected

340

to refine the program, the programmer prints information aboutthe choices being made by the angels.

Program P0′

angelicprint(input)while ( choose ) {

i = choose(n)j = choose(n)swap(i,j)angelicprint(i,j)

}assert isCorrect

The call angelicprint prints its arguments, but only on suc-cessful executions. Henceforth, assume that all of our programsare instrumented to print inputs and the indices of swapped peb-bles on safe executions into a log. The following is the output ofone of the safe executions of this program:

Input: bwrwrwb(0,2) (1,4) (2,5).

The following are swap sequences from two of the thousands ofother safe executions, one line per safe execution:

(4,0) (4,5) (2,1).(0,1) (1,5) (4,1) (2,0).

In examining the executions, the programmer focuses onwhich pebbles were swapped. Not surprisingly, he discovers thatthe highly nondeterministic program P0 swaps pebbles seem-ingly arbitrarily.

Now the programmer hypothesizes the first plausible imple-mentation strategy. Namely, he posits that it is possible to tra-verse the array of pebbles left-to-right while swapping the cur-rent pebble with a suitable counterpart.

Program P1 – refines Program P0

i = 0while ( choose ) {

swap(i, choose(n))i += 1

}assert isCorrect

In P1, a value that was previously provided by an angelicoperator (the first argument to swap in P0) is now computed bydeterministic code. We say that choose(n) was implemented bythis deterministic code. Technically, P1 is a refinement of P0 inthat it removes some of the nondeterministic choices present inP0. In other words, P1 generates a subset of executions of P0.

Program P1 is correct, too. Below, we show the log from oneof the safe executions.

Input: bwrwrwb(0,6) (1,0) (2,0) (3,1) (4,1) (5,3) (6,5).

P1 still generates too many traces (the angels have too muchfreedom) but the programmer notices a pattern in some of thesuccessful traces: towards the end of each run, the angels areswapping red pebbles to the left end of the array and blue peb-bles to the right end. The programmer hypothesizes that a more

on those inputs. Even if correctness is proven on all inputs, there is ofcourse no guarantee that a deterministic algorithm exists that can replacethe nondeterministic operators.

deterministic program would be explicit about gathering red peb-bles at the left end of the array and blue pebbles at the right fromthe very beginning.

This observation leads to an implementation strategy, en-coded in the next program, where the programmer implementstwo zones of uniform color: red at the left end of the array andblue at the right. The programmer further hypothesizes that azone of white pebbles in the middle of the array will simply “fallout” as a consequence. (The idea of three zones turns out to beexactly the insight that lead Dijkstra to his algorithm [6].)

Program P2

i = 0R = 0 // Pebbles to the left of R are redB = n−1 // Pebbles to the right of B are bluewhile ( choose ) {

c = pebbles(i) // examine colorif (c == red) { j = R; R += 1 }else if (c == blue) { j = B; B −= 1 }else /∗ (c == white) ∗/ { j = i }swap(i, j)i += 1

}assert isCorrect

In the next step, captured in program P2, the angelic opera-tor that provided the second argument to swap in P1 has beenreplaced by deterministic code. Program P2 is, however, not cor-rect, as it has no successful traces. P2 is therefore not a properrefinement of P1. Below is a log from a prefix of a failed trace:

Input: bwrwrwb(0,6) (1,1) (2,0) (3,3) (4,1).

Examining the log, the programmer can see a problem: afterthe swap (2,0), a blue pebble is brought into position 2, and thealgorithm then proceeds to examine the pebble in position 3,leaving the blue pebble in position 2. This pebble is never re-visited, and hence is never swapped to the right end of the arraywhere it belongs.

Generalizing from this observation, the programmer decidesthat in some cases a pebble brought into position i may need to behandled before moving on to position i+1. He decides to handlethis by not advancing i in some cases. Since the programmer isunsure of exactly when to advance i, he uses an angelic chooseoperator to make the decision.

The programmer returns to P1, which encoded an infeasibleimplementation strategy because it allowed at most one swap perposition of the array. The programmer creates P1a—an alterna-tive version of P1 that is a less restrictive refinement of P0.

Program P1a – refines Program P0

i = 0while ( choose ) {

j = choose(n)swap(i, j)if ( choose ) i += 1

}assert isCorrect

P1a is also correct.The programmer now again applies the refinement that intro-

duces zones of uniform color.

341

Program P3 – refines Program P1a

i = 0R = 0 // Pebbles to the left of this are redB = n−1 // Pebbles to the right of this are bluewhile ( choose ) {

c = pebbles(i) // examine colorif (c == red) { j = R; R += 1 }else if (c == blue) { j = B; B −= 1 }else /∗ (c == white) ∗/ { j = i }swap(i, j)if ( choose ) i += 1

}assert isCorrect

One log for this correct program, including the values of i aswell as swaps, is:

Input: bwrwrwbi=0:(0,6) i=0:(0,5) i=1:(1,1) i=2:(2,0) i=3:(3,3) i=4:(4,1).

The next step is to implement the nondeterministic operatorthat guards the advancement of i. Logs reveal that i is not ad-vanced when the current pebble (i.e., the ith pebble before theswap) is blue. On reflection, this is reasonable because when thecurrent pebble is blue, it is swapped with a pebble from fartherright in the array—a pebble whose color has not yet been exam-ined and is not yet known. In contrast, when the current pebbleis red, it is swapped with a pebble of a known color.

The programmer now implements the angelic operator withdeterministic code, creating refinement P4.

Program P4 – refines Program P3

i = 0R = 0 // Pebbles to the left of this are redB = n−1 // Pebbles to the right of this are bluewhile ( choose ) {

c = pebbles(i) // examine colorif (c == red) { j = R; R += 1 }else if (c == blue) { j = B; B −= 1 }else /∗ (c == white) ∗/ { j = i }swap(i, j)if (c != blue) i += 1 // changed from choose in P3

}assert isCorrect

The final refinement must implement the non-deterministicloop bound. The programmer might rashly replace the boundwith i < n. In this case, the programmer would find that theprogram is incorrect, because i would overrun the blue zone.Logs would reveal that the correct bound is i <= B, as shown inP5, which is the final deterministic program.

Program P5 – the final program; refines Program P4

i = 0R = 0 // Pebbles to the left of this are redB = n−1 // Pebbles to the right of this are bluewhile ( i <= B ) { // changed from choose in P4

c = pebbles(i) // examine colorif (c == red) { j = R; R += 1 }else if (c == blue) { j = B; B −= 1 }else /∗ (c == white) ∗/ { j = i }swap(i, j)if (c != blue) i += 1

}assert isCorrect

The development of this solution to the Dutch Flag problemshowed how the programmer can pose hypotheses about his im-plementation plans by writing and testing angelic programs and

how he can develop the final deterministic program by gradu-ally creating refinements that involve implementing angelic con-structs with deterministic code.

3. An Angelic Programming LanguageIn this section we describe the syntax and semantics of a smallprogramming language for writing angelic programs. Note thatour actual implementation is written as an extension to Scala.

3.1 Core languageWe consider the following core language:

stmt ::= v = expr (Assign)assert b (Assert)stmt1 ; stmt2 ; . . . ; stmtn (Sequence)if (b) { stmt } else { stmt } (Conditional)while (b) { stmt } (Loop)

expr ::= choose (Angelic)· · ·

Here v is a variable and b is a boolean-valued expression. (Thefull language supports arrays and heap operations as well, buttheir handling is routine and is ignored in this section.) A pro-gram fails on assert false.

The only source of nondeterminism in the language is An-gelic, which guesses a value in the domain of integers, addressesor booleans as appropriate in the context. The values of chooseexpressions are chosen so that, if at all possible, the executionterminates without failing any assert statement encountered inthe execution.

3.2 SemanticsFollowing previous work [3], we give semantics of this languageusing the wp predicate transformer [6]. Given a statement S anda postcondition R, wp(S, R) is the weakest precondition suchthat when S executes in a state satisfying that precondition, theexecution results in a state satisfying R. wp for statements in thecore language are straightforward:

wp(v = e, R) = R[e/v]wp(v = choose, R) = ∃v.R

wp(assert b, R) = b ∧R. . .

The wp for assignment substitutes free occurrences of v in thepostcondition with the expression e. The wp for choose bindsfree occurrences of v in R by existentially quantifying it: thisexpresses the notion that if possible, an angel will supply a valuesuch that R is true. The assert statement conjoins its conditionb to the postcondition, so that the program fails if b is false.The semantics of the control-flow statements are routine and areomitted.2

3.3 Program correctnessA program is assumed to be parameterized by an input vector !I ,which binds the initial values of variables. A program is assumedto check for expected postconditions using assert statements. Itis not necessary, though, that assert statements are placed only atthe end of a program.

A correct program is one for which ∀!I.wp(P (!I), true) is sat-isfiable. (We assume that all variables are defined before being

2 We assume that the programmer guarantees the existence of ranking func-tions such that loops terminate independently of choose expressions.

342

read, so that the formula above does not contain any free vari-ables.) The formula says that on any input there is a way in whichthe angel can choose values for choose expressions such that noassertion is violated in an execution of the program.

4. Traces and Program RefinementA trace is a sequence of values produced by choose expressionsduring a safe—i.e. not assert-failing and terminating—executionof a program. Given an input and a recorded trace, the execu-tion of an angelic program can be reconstructed faithfully. Be-cause the program’s postconditions may only care about the finalstate of variables, a particular input might result in multiple safetraces.

Define safetraces(P (!I)) to be the set of safe traces for aprogram P on input !I . P is correct if

∀!I.(safetraces(P (!I)) $= {})This correctness condition corresponds to the wp-based condi-tion from the previous subsection: each trace corresponds to thevalues which when assigned to existentially quantified variablesin the wp-based condition would satisfy the condition.

The purpose of refinement is to decrease dependence on theangel while carrying out effectively the same computation stepsas those computed by the angels. In particular, executions that aprogrammer ruled out by eliminating choices available to an an-gel (e.g., by adding assertions) should not reappear in successorprograms. If programs P and Q contain identical set of choosestatements, then for Q to refine P , it must be the case that:

∀!I.safetraces(Q(!I)) ⊆ safetraces(P (!I))

In general Q will not contain an identical set of choose ex-pressions as P , because a programmer typically eliminates somechoose expressions from P , implementing them in Q using de-terministic code or possibly using additional choose expressions.We define a set of program transformations that a programmer isallowed to use for refinements, and for which we give a suitablyadjusted trace containment property.

4.1 Program transformationsWe define three program transformations that, in combination,can be used to determinize an angelic program. Two of thetransformations (T1 and T3) are correctness-preserving, i.e., theypreserve safe traces of an angelic program, while the correctnessof T2 must be validated with a checker (see Section 4.2).

T1. State inflation Consider a pair of angelic programs P andQ. We say that Q is obtained from P by state inflation if Q addsprogram variables and statements (including choose) in such away that Q does not alter the traces of P : there is no change indata or control dependence of any of the statements of P . LetprojP represent a projection of safe traces of Q to the chooseoperators in P (all of which are also in Q). Then,

∀!I.projP (safetraces(Q(!I))) = safetraces(P (!I))

The role of this preparatory step is to introduce meta-data(a.k.a. “book keeping”) required to implement a choose expres-sion; an initial version of the program may not have included allthe meta-data it eventually needs.

T2. assert introduction If Q is obtained from P by adding anassert statement, where the (boolean) variable being asserted isone that the program already defines, we have the property that:

∀!I.safetraces(Q(!I)) ⊆ safetraces(P (!I))

This is true because the additional assert can only convert a tracethat was previously safe to unsafe.

T3. choose determinization If P contains choosev followedimmediately by assert (v = w) for some w, then we can replacethis pair of statements by assign v w to obtain program Q. Iftraces of P are projected to choose expressions that are commonto P and Q (using projQ) then:

∀!I.safetraces(Q(!I)) = projQ(safetraces(P (!I)))

This is true because the angel for choose v in P could haveproduced whatever value Q generates in variable w.

Example 1. Suppose we are developing a program to reversea linked list, for which we hypothesize the following angelicprogram works. The correctness postconditions (assertions) areomitted for clarity.

while (choose) {x = choose;y = choose;x.next = y;

}

Next, we might realize that we need a cursor variable cur. Weuse T1 to introduce it.

cur = inwhile (choose) {

x = choose;y = choose;cur = cur.next;x.next = y;

}

Now we add an assert using T2 to constrain one of the angelicchoices to use this cur variable.

cur = inwhile (choose) {

x = choose;assert x == cur;y = choose;cur = cur.next;x.next = y;

}

We can then use T3 to determinize and remove this chooseexpression.

cur = inwhile (choose) {

x = cury = choose;cur = cur.next;x.next = y;

}

4.2 Trace-based RefinementFormally, we say P is refined by Q, denoted P & Q, if

1. Q is derived from P using zero or more of the above trans-formations, and,

2. Q is correct, i.e. ∀!I.(safetraces(Q(!I)) $= {}).

343

The refinement relation & is reflexive and transitive.Note that assert-introduction may cause the second condition

to be violated. In this work, our intention is to use bounded-model-checking to establish the second condition.

The goal of refinement is to reduce the nondeterminism in aprogram. While state inflation of P into Q can introduce into Qnew choose expressions, the non-determinism in Q with respectto P cannot increase with refinement because we do not reduceconstraints on the choose expressions already present in P . Thisis because constraints on nondeterminism are relaxed only whenstatements are removed (in T3) and we remove statements onlywhen their effect is already captured by an assertion.

Program development can be seen as creating a series ofprograms, P0, . . . , Pn, such that Pi & Pi+1, 0 ≤ i < n, whereP0 is the first angelic program a programmer creates, and Pn

is free of any choose statements, i.e. a suitable deterministicprogram that takes the intended computation steps.

Existence of such a sequence does not mean that a program-mer will make only correct refinements. In practice, a program-mer will occasionally need to revert to a previous version and trysome other transformation.

Adhering to the trace refinement methodology brings valu-able software engineering benefits to the practical programmer;we explain these in the next section.

4.3 Program RestructuringSometimes a programmer might wish to refine a partial programin a way that is not expressible in terms of transformations T1–T3. Suppose s is a statement in P . In program Q, we wish toreplace s with a statement s′. We assume that s is choose-free buts′ is permitted to introduce new choose expressions.3 Program Qwill be a refinement of P if Q is correct and s′ does not affordthe choose operators in Q more freedom than they had in P . Thesecond condition corresponds to ensuring that Q does not allownew traces:

∀!I.projP (safetraces(Q(!I))) ⊆ safetraces(P (!I))

To ensure this property, we enforce, by construction, twoproperties:

1. The input values of s and s′ are the same in all safe execu-tions. Assume that s′ has the same free variables as s. Thisproperty ensures that any choose expressions executed priorto s are constrained in Q at least as much as they were in P(the constraints are the union of constraints imposed by s ands′, respectively).

2. The output values of s and s′ are the same in all safe exe-cutions. Assume that s and s′ update identical “output” vari-ables. This ensures that s and s′ end up in the same state,ensuring that yet to be executed choose expressions in Q willbe constrained at least as much as they were in P (the con-straints are the union of constraints of s and s′, respectively).

Note that s′ is free to contain any implementation; we do notimpose any constraints on the values computed at intermediatepoints in s′.

The next two transformations are intended to allow safe re-structuring.T4. Coupling The goal of the construction is to assert equalityon input values to s and s′, as well as on their output values. This

3 Note that occurrences of choose in the statement s can be removed bylifting all instances of choose into a special argument of s, a list angels,whose elements are read by s.

is done by recording these values in P and asserting their equal-ity in Q. We call this transformation coupling of programs P andQ. Assume we can establish communication channels betweenP and Q. Each channel supports a snd(v) operation to send avalue and a rcvandassert(v) operation that receives a value andasserts it to be equal to the value given as its argument. Just be-fore executing s, P communicates all its “input” values (to s)to Q using a series of snd operations. Q receives them usingrcvandassert operations just before entering s′, supplying its in-put values in the same sequence. Likewise, just after executing s,P communicates its “output” values over to Q, which comparesthem against its own output values.

T5. Uncoupling When the refinement process arrives at achoose-free program Q, the channels attached to Q are discon-nected and the ancestor programs on the other end of the chan-nels are discarded. Because Q is choose-free, the constraints im-posed by the channels are not needed as they are already implicitin Q.

Henceforth, we consider this construction as part of our re-finement toolset.

5. Programming with AngelsWe assume that the programmer starts with a safe angelic pro-gram. Note that he may start with a relaxed correctness condi-tion, strengthening it during the refinement process.

5.1 MethodologyAs mentioned before, a programmer applies a succession of tracerefinement steps until a program does not contain any chooseexpressions. Tool support helps in two ways:

1. Testing hypotheses. When the programmer carries out a trans-formation (T2 or T4) that adds constraints on the nondeter-ministic choices, he checks whether the resulting programmaintains safety. If the program is not safe, the implemen-tation strategy encoded in the angelic program is infeasible.In our implementation, this validation is performed with abounded model checker.

2. Trace demonstration. The tool outputs the safe traces, whichthe programmer can examine to learn about the steps taken bythe angel. In our case studies, these traces provided importantclues as to what further refinement steps to take.

We remind the reader that execution of the angelic programis carried out on a small suite of test cases, which means that wemay fail to ascertain that an angelic program (or the final pro-gram) is unsafe. As a result, it is possible that the programmermakes incorrect refinement decisions. In this sense, we are pro-viding no more guarantees than the classical (test-driven) pro-gram development on which we wish to improve. However, ascorroborated by work on using bounded model checking for soft-ware validation [8], a small test suite often reveals many softwareerrors.

Example 2. This example illustrates that the programmer candiscover that his implementation strategy is infeasible withoutnecessarily having to obtain an incorrect angelic program. Con-sider Example 1, where we partially refined an angelic programfor linked list reversal. If our initial refinement step in Example 1had been slightly different, we would have produced followingangelic program:

344

cur = inwhile (choose) {

x = cury = choose;x.next = y;cur = cur.next;

}

This program differs from the program produced in Example 1 inthat the last two statements inside the loop have been switched.It is easy to write a program like this one (we did), consideringthat programmers are used to incrementing the loop inductionvariable at the very end of the loop.

This angelic program encodes an infeasible strategy. Specif-ically, the program contains a bug in that it overwrites cur.nextbefore the old value of cur.next is read and used to obtain thenext node. As a result, the angelic program is prevented fromwalking forward down the list. The angelic program is not incor-rect, though, because some safe traces remain. For instance, inthe first loop iteration, the program can “travel” to the last nodeand then walk the list backwards. The programmer can noticethat no safe traces correspond to a desired algorithm by observ-ing that all traces require at least n + 1 steps for a list with nelements. If desired, this observation can be confirmed by limit-ing the angelic program to n loop iterations, at which point nosafe traces remain. The programmer then reverts to an earlier ver-sion of the program and corrects the implementation of how theinduction variable cur is advanced through the list.

5.2 Removing angelic dependencesInformally, we say a choose expression c1 is independent from achoose expression c2 if c1 can make its choices without consid-ering what choice has been (or will be) made by c2 in a given ex-ecution. Angelic programs with only independent choose expres-sions may lead to simpler algorithms because there is no need todeterministically implement the “communication” that happensbetween dependent choose expressions. We leave the full charac-terization of angelic dependence for future work. Here, we giveonly a sufficient condition for independence and discuss one ofits benefits.

A sufficient condition for independence of all choose expres-sions in a program is that the program has only one safe trace perinput. If a choose expression has the choice of only one value atany time it is evaluated, it need not consider the choices made byother angels.

When angelic choices are independent we get the benefit thatchoose expressions can be refined independently of each other.That is, the implementation need not consider how the otherchoices are determinized, as the next example illustrates.

Example 3. In Example 2, by examining traces returned bythe oracle, the programmer recognized that it might be usefulto enforce that the loop that reverses the elements in a linked listexecutes n times for a list with n elements. He might enforce thisby writing the following program (with the bug from Example 2fixed).

cur = inwhile (n) (choose) {

x = cury = choose;cur = cur.next;x.next = y;

}

Here, n is the number of elements in the list and while (n) issyntactic sugar for a loop that performs no more than n iterations.

This program has exactly one trace (to see this, note that eachof the n nodes in the list must have its next field overwritten,and by design the loop iterates at most n times, so to generate asafe trace the oracle must set each node’s next field to the cor-rect final value). With this, we are guaranteed to have removedthe angelic dependence mentioned above. The programmer canthus consider implementing the two remaining angelic choicesseparately.

The loop condition is relatively easy: we know that we mustensure that cur is not null, and by examining the traces returnedby the oracle we can see that this is indeed the correct condition.We can thus use T2 and T3 to generate the following program.

cur = inwhile (n) (cur != null) {

x = cur;y = choose;cur = cur.next;x.next = y;

}

For the remaining angelic choice, by examining the safe tracewe notice that y is always chosen as the element before cur inthe original list. With this insight, we can recognize that we mustuse T1 to introduce a new variable and have it walk down the list(as well as remove the now-unnecessary bounded loop), T2 toassert that y is chosen to be equal to it, and then T3 to generatethe following fully-deterministic correct program.

cur = inprev = nullwhile (cur != null) {

x = cur;y = prev;cur = cur.next;x.next = y;prev = x;

}

5.3 Example of Program RestructuringThe next example illustrates transformations T4 and T5. Recallthat, in contrast to T1–T3, the transformation T4 does not im-plement an instance of choose, but instead substitutes some de-terministic code in program P with another (potentially angelic)code. The constraints on nondeterminism present in P are im-posed on Q using communication channels between the two pro-grams. T5 is a cleanup transformation.

Example 4. We partially develop an implementation of theZipReverse problem assigned by Olivier Danvy at a sum-mer school. Given two lists x and y, the problem is to com-pute zip(x, reverse(y)). For example, given x = [1, 2, 3, 4] andy = [a, b, c, d], the program outputs [(1,d), (2,c), (3,b), (4,a)].The requirements are: neither list can be traversed more thanonce; the lists cannot be copied; the length of the lists is notknown a priori; and lists must be manipulated with the low-leveloperations car, cdr and cons.

The first angelic program tests a hypothesis that the result canbe computed with a sequence of cons operations. When x is a list,choose(x) selects an element from the list.

345

r = nilwhile (choose) {

r = cons((choose(x), choose(y)), r)}

The hypothesis tested by the program is admittedly trivial butthe program is nonetheless a useful start for refinement. This isbecause it has only one safe trace for each input. Consideringthat a list can only be created with cons, this trace shows thesteps that final implementation will have to take.

The programmer then attempts to refine this program by im-plementing choose(x) and choose(y) with code that traverses thetwo lists by maintaining pointers that advance down the lists. Thelists are traversed according to the restrictions of the problemstatement but nondeterminism allows all legal traversals. Thisattempt at refinement, not shown, has no safe trace, so the pro-grammer concludes that a recursive procedure needs to be used inplace of the iterative one. This recursive angelic program, shownbelow, can be refined into a deterministic program; thanks torecursion, the list y can be traversed in the opposite direction,which was not possible in the iterative version.

r = nildescent()

def descent() {if (choose) returndescent()r = cons((choose(x), choose(y)), r)

}

How is this program a refinement of the first program? Theprogrammer can make it so by invoking transformation T4. Theprogram that couples the two programs can be written as follows:

r = nilch1 = choosesnd(ch1)while (ch1) {

ch2 = choose(x)snd(ch2)ch3 = choose(y)snd(ch3)r = cons((ch2, ch3), r)ch1 = choosesnd(ch1)

}ch4 = pack(r)snd(ch4)

||

r = nildescent()

def descent() {ch1 = choosercvandassert(ch1)if (ch1) returndescent()ch2 = choose(x)rcvandassert(ch2)ch3 = choose(y)rcvandassert(ch3)r = cons((ch2, ch3), r)

}ch4 = pack(r)rcvandassert(ch4)

The program on the left is the first program, with insertedchannel send operations; on the right is the second program, withinserted channel receives. The output of the statement that wehave replaced with T4 is the list r, which is packed with pack(r)before it is sent across a channel. (The output of the secondprogram is considered to be the output of the entire program.)

Superficially, we violated the conditions of T4: the programon the the left contains choose statements while T4 requires thatthe replaced statement be choose-free. However, conceptuallywe could have collected angelic values in advance in an arrayangels and read from it.

Once the program on the right is developed into deterministiccode (not shown for lack of space), we can use T5 to disconnectthe program on the left and remove the channels.

6. Comparison to Deductive RefinementWe briefly review Morgan’s [11] refinement methodology, andthen contrast it with ours. We note at the outset that Morgan’smethodology is geared towards developing a proof of a programas it is being developed, whereas our intention is to help theprogrammer develop a program that is correct within the confinesof bounded model checking.

In Morgan’s methodology, one develops programs using re-finement of specification statements. A specification statement isof the form !v : [pre, post ], and it can be used any place an or-dinary statement may be used. The meaning of this statement isthat, if executed in a state in which pre holds, it modifies statevariables in !v such that post holds. The specification statementis a demonic non-deterministic statement: all values that satisfypost must suffice. The wp-semantics of a specification statementare given as:

wp(!v[pre, post ], R) = pre ∧ (∀!v.post =⇒ R)

Refinement of a specification statement is either a determin-istic statement or another specification statement that could beused in place of the original one. For P to be correctly refined byQ, denoted P ≤ Q,

∀R,wp(P, R) =⇒ wp(Q, R)

where wp is the weakest precondition operator and R is a post-condition.

Semantically, our definition of trace-based refinement (&) isa special case of the refinement defined above (≤).

In Morgan’s methodology, a programmer makes a sequenceof refinement steps until the program is completely free of non-deterministic statements. Rather than prove correctness of refine-ment steps from the principle above, the programmer can use aset of proven refinement laws, some of which we show here:

• It is always legal to strengthen the postcondition or weakenthe precondition.

• One of the laws for introduction of a sequential compositionrequires a suitable intermediate condition (mid below): !v :[pre, post ] −→ !v : [pre,mid ];!v : [mid , post ]. The midshould be suitable in that further refinement of the two newspecification statements is feasible.

• The law for introduction of a loop requires a specification tobe brought in of the form: [pre ∧ I, post ∧ I], where I is asuitable loop invariant. (We omit the actual transformation;see [11].)

An attractive property of Morgan’s refinement is that occur-rences of specification statements in a program can be refinedindependently of each other (Theorem 2, pg 8, in Morgan andVickers [12]). This makes the approach compositional.

However, from the perspective of a practical programmer, thisis also the main problem in using Morgan’s methodology: in-dividual specification statements must have sufficiently power-ful postconditions and correct invariants for the programmer tomake progress. If the programmer does not figure out the correctloop invariant, the methodology might get stuck later during de-velopment, and even the correct invariant must often be modifiedin later steps. This is not surprising: a programmer is developinga rigorous proof simultaneously with the program.

Angelic specification statements have also been suggested inthe literature (e.g. Celiku and Wright [4], Ward and Hayes [20]).Ward and Hayes used angelic refinement to prove correctness ofbacktracking algorithms. Let us denote an angelic specification

346

statement as !v : {pre, post}, meaning that the !v must be assignedin such a way that post holds, and moreover, the values are cho-sen cooperatively to make the rest of the program run correctly.Formally,

wp(!v{pre, post}, R) = pre ∧ (∃!v.post ∧R)

At first glance, this seems to free the programmer from thenecessity of providing suitable post, because the angel would al-ways select a value that suits the rest of the computation (in addi-tion to satisfying whatever postcondition is provided manifestlyin the specification statement.). However, local refinement of an-gelic specifications, i.e. a refinement that can be applied oblivi-ous to the rest of the program, is only allowed to increase choicesavailable to the angel. This does not help the programmer to getto a deterministic program. (As stated above, their purpose wasto prove correctness of backtracking algorithms.)

Celiku and Wright propose strengthening the postconditionsof angelic specifications in such a way that they essentially be-come demonic specifications, which can then be refined usingMorgan-like methodology. The process of converting angelic todemonic specification requires manual, non-local, whole pro-gram reasoning. More formally, the following conversion can becarried out for a strong enough φ:

!v{pre, post}; assert φ −→ !v[pre, post ∧ φ]

The problem for the programmer is to determine the φ that wouldcapture the “expectation” of the rest of the program.

Our methodology is intended as a practical tool for programdevelopment, not as a proof procedure. In our methodology, wedo not have compositionality: by default, angelic choose expres-sions cannot be refined obliviously to each other. (Removing an-gelic correlation, as we discussed in Section 5.2, is similar toconverting angelic to demonic nondeterminism.)

7. ImplementationWe have embedded the angelic choice construct into the Scalaprogramming language [13]. The programmer passes to the an-gelic choice operator a list of values from which a parallel back-tracking solver selects one that leads to a safe trace. The solvercomputes all safe traces, which can then be browsed with a userinterface. The angelic choice operator ranges over arbitrary prim-itives or references to heap-allocated variables and can be usedon arbitrary programs. We have used our implementation to de-velop the examples in this paper as well as several others. Therest of this section discusses our backtracking solver, as well as amore scalable SAT solver. We conclude by comparing their scal-ability.

7.1 Parallel Backtracking SolverOur backtracking solver performs a depth-first traversal over thespace of traces, searching for safe traces. The solver executes aprogram and whenever it encounters a choice operator, it pushesa new entry corresponding to the dynamic instance of this opera-tor to a stack. It then tries the first value for the entry on the top ofthe stack by executing the program further. On an assertion fail-ure, the solver backtracks to the execution point associated withthe top of the stack and tries the next value. If there are no valuesleft for this entry, it is popped from the stack and the executionbacktracks to the execution point corresponding to the previousentry on the stack, continuing the process. A safe trace is foundwhen the execution terminates without failing an assertion.

Figure 1. Time taken (in seconds) by the SAT solver and thebacktracking solver when inserting a number of nodes into aninitially empty binary tree.

Our backtracking solver explores the trace space in parallel.Each task receives from the dispatcher a prefix of a trace, includ-ing the corresponding stack that captures the state of the angelicoperators executed on that prefix. The stack captures which val-ues have already been tried for these operators, and it is thus anefficient way of representing the part of the space that has alreadybeen explored. Each parallel task is responsible for exploring allsuffixes of the prefix. Except for communicating with the dis-patcher, the tasks are independent, so the parallel computation isvery efficient. Our solver is efficient in its memory use so mostof its time is spent executing the program.

Backtracking is not a good fit for problems that contain as-sertions mostly at the end of the execution because the searchtakes a long time to encounter a failure. SAT solvers, on theother hand, propagate constraints and can generate new con-straints (conflict clauses) as they explore the search space. Thebacktracking solver’s scalability on these problems can be im-proved by adding assertions with intermediate invariants, whichcan drastically prune the search space.

7.2 SAT-based Angelic SolverOur second solver is based on symbolically unrolling the pro-gram for given inputs and representing the bounded execution asa SAT formula. The satisfying assignment then gives the val-ues of the angelic operators on these inputs. We use the pro-gram translator developed as part of the SKETCH synthesizerproject [18], which achieves scalability by representing integersin sparse unary encoding, thus optimizing the SAT formula forthe small values that one is likely to see during program devel-opment based on unit testing. This solver is not yet connectedto our Scala-based frontend but angelic programs for that solvercan be written in the SKETCH language by using simple macros.

7.3 Solver Efficiency StudyAll experiments presented in this paper were performed onthe backtracking solver, whose scalability was sufficient (itsresponse was usually interactive). It is interesting to evaluatewhether the SAT-based angelic solver allows us to scale to harderproblems. While individual problems vary widely, some exper-imental performance numbers may be helpful to compare thescalability of angelic solver techniques. In Figure 1, the timeof the backtracking and SAT solvers is shown on an angelicprogram that inserts nodes into a binary search tree. The an-gel chooses any node in the tree and then chooses whether toinsert the new node below the left child or the right child. As-suming that we only insert into nodes already in the tree, thisis a search space of 2n−1n!, where n is the number of nodes

347

to insert. There is only one correct solution in this space. Thebacktracking solver is usable until the search space is about 109,while the SAT-based angelic solver scales to 8 ∗ 1020, or morethan ten orders of magnitude higher. This gives us hope that an-gelic refinement based on executable agents may be usable on arange of realistic programs.

8. Case Study: Modularizing DSWThis section describes a case study where angelic programmingaided in modularizing the Deutsch-Schorr-Waite (DSW) graphmarking algorithm [15]. This algorithm has long been consid-ered one of the most challenging pointer algorithms to reasonabout [2]. We modularize DSW with a new generic abstraction,called the parasitic stack, which is parameterized for DSW withangelic operators. Our case study shows that (i) modulariza-tion of DSW simplifies reasoning about the algorithm becausethe parasitic stack separates concerns intertwined in DSW; and(ii) choose operators allow the algorithm designer to sidestepglobal reasoning. We conjecture that modularization by angel-ically parameterizing an abstraction may be applicable morewidely to other complex algorithms.

The DSW algorithm solves the problem of marking nodesreachable from the root of a directed graph. The crucial re-quirement is to do so with only constant-size additional stor-age. (One exception is that a node can store an index into itslist of children.) This requirement is motivated by garbage col-lection where object tracing is invoked when the system cannotoffer more than constant-size memory. If linear-size additionalmemory were available, one could perform a DFS traversal ofthe graph, using a stack for backtracking. The DSW trick is toguide the DFS traversal by temporarily “rewiring” child point-ers in nodes being visited. The price for the space efficiency isintertwining of the two parts of the DFS traversal:• Iterative graph traversal. Because recursive DFS traversal is

ruled out, DSW relies on the more involved iterative traversal.• Backtracking structure. Because an explicit stack is ruled out,

DSW encodes a backtracking structure in the child pointersof graph nodes.

Ideally, these aspects should be separated, for example by hid-ing the backtracking structure under an abstraction. However,as shown in Figure 2, this separation seems difficult. We donot ask the reader to understand the algorithm in Figure 2;we merely want to point out the essence of what complicatesmodularization. In particular, note that the memory locationcurrent.children[current.idx] serves two roles: on the right-handside of the first parallel assignment, the location stores an edgeof the graph; on the left-hand side, it serves as storage for thebacktracking structure. Because the role of the location changesin the middle of an assignment, it is not obvious how to hide onerole under a procedural abstraction.

The original goal of our case study was not to modularizeDSW; we started by asking simply whether angelic program-ming could aid in discovering and implementing the classical(flat) DSW algorithm. We explained the trick in DSW to a fewstudents and then gave them access to the Scala language ex-tended with choose operators. The insight was explained bytelling them to “use the child fields in the graph nodes to encodethe backtracking stack needed in DFS traversal.” As students ex-plored algorithmic ideas, it became clear that it was difficult todesign DSW even with angelic nondeterminism. The reason wasthat the programmer found it hard to explain how the angels ma-

def DSW(g) {val vroot = new Node(g.root)var up, current = vroot, g.root

while (current != vroot) {if (!current.visited) current.visited = trueif (current has unvisited children) {

current.idx = index of first unvisited child// the child slot changes roles in this assignmentup, current, current.children[current.idx] =

current, current.children[current.idx], up} else {

// the child slot restores its roleup, current, up.children[up.idx] =

up.children[up.idx], up, current}

}}

Figure 2. The classical (flat) DSW algorithm.

def DSW(g) {val vroot = new Node(g.root)var current = g.rootParasiticStack.push(vroot, List(vroot,g.root))

while (current != vroot) {if (!current.visited) current.visited = trueif (current has unvisited children) {

current.idx = index of first unvisited childval child = current.children[current.idx]ParasiticStack.push(current, List(current, child))current = child

} else {current = ParasiticStack.pop(List(current))

}}

}

Figure 3. The parasitic (modular) DSW algorithm.

nipulated the individual pointer fields. It became apparent that itwas necessary to raise the level of the programming abstractionso that one could observe the angelic decisions at a more mean-ingful semantic level. Unfortunately, as we pointed out, DSWdoes not seem to permit such modularization.

It occurred to us that it might be possible to express DSW asusing a special stack-like data structure. Like a regular stack, thisdata structure would support the push and pop operations withthe usual semantics. Unlike a regular stack, this data structurewould be implemented by borrowing memory locations from itshost — in our case, the graph being traversed. The hope was thatthe metaphor of borrowing a location would allow us to expressthe transition between roles in a systematic fashion. We termedthis data structure a parasitic stack because it borrows locationsfrom its hosts and eventually restores them (parasites do notdestroy their host). The challenge was how to express DSW witha parasitic stack, if that was at all possible.

The parasitic stack has the interface shown below. As usual,the pop operation returns the value x stored in the correspondingpush operation. The nodes argument to push passes into the stackthe environment of the parasitic stack’s client. Through this ar-gument, the traversal code “loans” the nodes referenced by in-scope variables to the stack. The environment is also passed intopop, where the values may be useful for restoring the values of

348

ParasiticStack {e = new Location // constant−size storage (one location suffices to support DSW)

push(x,nodes) { // ’nodes’ is the set of nodes offered "on loan" by the host data structuren = choose(nodes) // angelically select which node to borrow from the host ...c = choose(n.children.length) // ... and which child slot in that node to use as the locationn.idx2 = c // remember the index of the borrowed child slotv = n.children[n.idx2] // read the value in the borrowed location; it may be needed for restoring the borrowed slot in pop()// angelically select values to store in the two locations available to us (’e’ and the borrowed location)e, n.children[n.idx2] = angelicallySemiPermute(x, n, v, e)

}pop(nodes) {

n = choose(nodes, e) // the borrowed location better be in either ’e’ or in ’nodes’v = n.children[n.idx2] // ’v’ is the value we stored in the borrowed location// select what value to return and update the two locations we work with (the borrowed child slot and ’e’)r, n.children[n.idx2], e = angelicallySemiPermute(n, v, e, ∗nodes) // ’∗nodes’ unpacks arguments from list ’nodes’return r

}}

Figure 4. An angelic implementation of the parasitic stack, ParasiticStack0.

borrowed memory locations when the stack returns them to thehost.

// parasitic stack can borrow a field in some node from ’nodes’push(x:Node, nodes:List[Node])// values in ’nodes’ may be useful in returning storage to hostpop(nodes:List[Node]) : Node

With the parasitic stack in hand, DSW can be expressed asshown in Figure 3; it can be derived almost mechanically from arecursive DFS traversal procedure. If one ignores the additionalarguments to push and pop, this parasitic DSW appears to use aregular stack that has private storage. The parasitic DSW is thusmodular in that it abstracts away the details of how the parasiticstack is implemented.

Note that at this point of our user study, we did not yet knowwhether DSW was expressible on top of such a stack interface.The challenge behind answering this question was to determine

• which location the parasitic stack can borrow from the host—it must be a location the host does not need until the locationis returned to the host by the parasitic stack;

• how to restore the value in this location when returning it tothe host; and

• how to use this location to implement the push/pop interface.

These three questions are formulated as nondeterministicchoices in the implementation of the parasitic stack. We werenot able to answer these questions without angelic help.

The reader might wonder if we simply substituted one hardproblem, namely implementing DSW using low-level pointermanipulations, with another equally hard one, namely imple-menting the parasitic stack. What we achieved is that an angelicformulation of the three questions (for parasitic stack) is rela-tively straightforward. Furthermore, we found that the angelicanswers to these questions can be interpreted by the program-mer more easily than when the angels implement the low-levelDSW pointer manipulations because the questions raise the levelof abstraction.

The three questions are encoded in the angelic implementa-tion of the parasitic stack, shown in Figure 4. As we discussthis code, it will be obvious that the three questions, answeredby angels, capture the global reasoning necessary to make the

parasitic stack operate correctly. As a result, the programmer isfreed to constrain himself to local reasoning, whose goal is to de-scribe how a generic parasitic stack might operate. Genericity isachieved with angelic nondeterminism. The next two paragraphsdescribe the angelic parasitic stack. We then use refinement toparameterize this angelic parasitic stack for DSW.

The parasitic stack in Figure 4 keeps only a single memory lo-cation (e). The push method first angelically selects which mem-ory location to borrow from the host. This is done by selectinga suitable node n and a child slot c in that node. The borrowedlocation is n.children[c]. The stack (deterministically) stores theselected child slot index in the node itself, as that is allowed bythe constraints of the DSW problem. Next, push reads the valuein the borrowed location since it will need to be restored later andso may need to be saved. Finally, there is a hard decision. Thestack has four values that it may need to remember: the pushedvalue x, the reference to the borrowed location n, the value inthat location v, and the value in the extra location e. However,there are only two locations available to the stack: the borrowedlocation n and the extra location e. Clearly, only two of the fourvalues can be stored. Perhaps the value of e is not needed, but theremaining three values are essential.

A little reasoning reveals that the parasitic stack is plausibleonly if the value that push must throw away is available at thetime of pop from the variables of the enclosing traversal code.Therefore, we decided to make the environment of the clientavailable to pop. The pop method first guesses which locationwas borrowed in the corresponding push. This location is eithern or is in nodes; no other alternatives exist. Next, pop readsthe value from the borrowed location. Finally, pop angelicallydecides (i) which value to return, (ii) how to update the extralocations, and (iii) how to restore the borrowed location. As inthe case of push, it must select from among four values.

The benefits of clairvoyance should now be clear: while thehuman found it hard to make the global decisions necessaryto instantiate the parasitic stack for DSW, these decisions wererather straightforward to formulate as nondeterministic choices.

Next, we instantiated the parasitic stack for DSW by refin-ing it into a deterministic program. To arrive at the first refine-ment, we observed how the angels permuted the values. We iden-tified a trace in which the angels performed the same permuta-tion throughout the entire execution, except in the first call to

349

push. This first call was an outlier because our parasitic DSWalgorithm (Figure 3) originally invoked the first push incorrectly,with the reversed environment, as follows:

ParasiticStack.push(current, List(g.root,vroot)).After we modified the parasitic DSW, we were able to implementthe permutations with deterministic code, shown below. We no-ticed that the value v of the borrowed location was not saved bythe angel and that it was later obtained from nodes[0] in pop.This answered the question of how to restore the value in theborrowed location.

ParasiticStack1 – refines ParasiticStack0

ParasiticStack {e = new Location

push(x,nodes) {n = choose(nodes)c = choose(n.children.length)n.idx2 = c// rhs was ’angelicallySemiPermute(x, v, e, n)’e, n.children[n.idx2] = x, e

}pop(nodes) {

n = e // rhs was ’choose(nodes, e)’v = n.children[n.idx2]// rhs was ’angelicallySemiPermute(n, v, e, ∗nodes)’r, n.children[n.idx2], e = e, nodes[0], vreturn r

}}

To arrive at the second refinement, we observed how theangels selected the borrowed node (the value n in push). Wetested if the choice was consistent across all instances of push(it was), and then we implemented the angelic choice.

ParasiticStack2 – refines ParasiticStack1

ParasiticStack {e = new Location

push(x,nodes) {n = nodes[0] // rhs was ’choose(nodes)’c = choose(n.children.length)n.idx2 = ce, n.children[n.idx2] = x, e

}pop(nodes) { ... unchanged ... }

}

To perform the last refinement step, we examined how the re-maining angel selected the child slot to borrow from the selectednode n. We learned that it selected a slot whose value equalednodes[1], the second variable passed to push from the traversalcode. This implied that c equaled the value of n.idx maintained inthe traversal code. Therefore, maintaining a separate field n.idx2was not necessary. We turned this observation into the determin-istic code shown below. This completed the construction of par-asitic DSW.

ParasiticStack3 – refines ParasiticStack2

ParasiticStack {e = new Location

push(x,nodes) {n = nodes[0]// invariant: n.children[c] == nodes[1],// hence c == n.idx == n.idx2// was ’c = choose(n.children.length); n.idx2 = c’e, n.children[n.idx] = x, e

}pop(nodes) {

n = ev = n.children[n.idx] // was n.idx2r, n.children[n.idx], e = e, nodes[0], vreturn r

}}

Angelic nondeterminism has simplified implementation ofthe DSW algorithm. The parasitic DSW still takes some ef-fort to understand but the comprehension task has been brokendown into three smaller questions: how the stack is implemented,which location the stack is borrowing, and how its value is re-stored. It would be interesting to consider whether this modular-ization of DSW leads to a simpler deductive proof of correctness.

9. Related WorkFloyd was one of the first to propose using angelic nondetermin-ism as a programming construct [7]. The concept appears also informal languages, e.g., in nondeterministic automata [14]. An-gelic non-determinism also allowed abstracting specifications bynon-deterministically coordinating concurrent components [16].In the rest of this section, we focus on work related to systematicprogram development.

J-R. Abrial has presented a very successful methodology forconstruction of event-based systems [1] and showed that thesame methodology works for pointer manipulating programs [2].Abrial’s methodology starts with a declarative specification ofthe desired program and gives a set of laws by which specifi-cations can be translated to lower-level specifications until theyreach the level of basic program statements orchestrated by theusual control-flow constructs. At each step, his methodologyasks a programmer to discharge a number of proof obligations,some of which can be automated. However, the methodology isintended primarily for programmers who are well-versed in theuse of automated theorem provers.

His derivation of DSW does not modularize the constituentconcepts of the backtracking structure, the graph, and the traver-sal order [2]. He defines the backtracking structure as closelytied with the traversal order: the structure is a list of nodes cur-rently being visited. This characterization is then refined by im-plementing the list as a rewiring of the graph. Therefore, thecontent of the structure remains intimately linked with the graphstructure.

Angelic nondeterminism has been used in refinement-basedprogram development methodology proposed by Back, Wright,Morgan, and others. Back and von Wright [3] simplified problemdecomposition in program refinement [11], using angelic nonde-terminism to satisfy intermediate conditions that the programmerwould be able to spell out only later in the development process.

In contrast to deductive angelic refinement, our angelic pro-grams are executable, as in Floyd [7]. This allows a test of thecorrectness of angelic programs. If the test fails, we have con-clusively proved that the angelic program is incorrect and cannot

350

be successfully refined. A passing test is not conclusive (we relyon bounded model checking [5]), but the executable angel showsus a demonstration of how to execute the program. Safe angelictraces thus serve as demonstrations of what steps the angel takesto execute the program, which has the benefit of increasing pro-gram understanding.

Celiku and Wright [4] show how to refine angelic nondeter-ministic statements to demonic ones, with the goal of being ableto refine them independently. In order to achieve this goal, theyprove, manually, sufficient postconditions that an angelic state-ment must satisfy. In general, however, angelic correctness is es-tablished with a proof that is obtained by effectively completingthe refinement process all the way to the deterministic program.While such a deductive process provides a proof of the finalprogram, it does not seem to enhance programmer productivity.Our approach changes the problem: we side-step the intermedi-ate stage of obtaining an equivalent demonic nondeterministicprogram. Instead, we aim to refine to a deterministic programdirectly, which we achieve by making the choose operator exe-cutable.

There has been a long tradition of specification-based pro-gramming at Oxford University, and Morgan’s work representsthat school of thought. The Z programming methodology [19] isclosely related to that of Morgan. A complete description of re-lated work in this tradition is well outside the scope of this paper,but [11] is a good reference.

The work presented in this paper can be viewed as an evolu-tion of the SKETCH project [18, 17]. In SKETCH, a program-mer leaves syntactic “holes” in a program, which can later befilled automatically by a family of expressions. SKETCH hasbeen shown to work very well in several application domains, inparticular bit-manipulating programs, in which it is easy to givea specification of an unoptimized program but difficult to de-velop a correct optimized program. One of the limitations of theSKETCH work is that it requires a substantial amount of workon the part of a programmer to write the incomplete program,since holes are substitutes for only a very limited family of ex-pressions. This not only is work for the programmer, but it alsocreates the possibility of making human mistakes that can causea SKETCH synthesizer to not be able to fill in the holes. Thepresent work addresses both criticisms.

The work presented in this paper was in part inspired by thework of Lau et al., who synthesize editing macro programs fromuser demonstrations of executions (i.e., traces) of the desiredmacros [9]. We realized that such demonstrations are useful notonly for synthesis but also for gaining understanding about analgorithm that is under construction. In fact, if an oracle gavethe programmers a trace of the program that they are developing,they might find it easier to debug the program as well as developit in the first place. Program development could then be viewedas generalizing the algorithm from demonstrations. Our nextobservation was that such oracular traces could be created byangelic programs, which led to the work presented in this paper.

10. ConclusionWe have demonstrated that an executable implementation of an-gelic nondeterminism may help in program development. First,the programmer can evaluate hypotheses about his implementa-tion strategy by testing whether his incomplete program can beexecuted angelically. If angelic operators cannot complete theprogram with values that lead to a successful execution, neitherwill be the programmer, and so the program follows an infeasi-

ble implementation strategy. Second, if an incomplete programcan be completed angelically, the successful angelic executionsmay reveal steps that the incomplete algorithm should follow.The traces computed by the angelic operators may thus lead theprogrammer to an a-ha moment. Third, angelic operators supportrefinement-based programming, where the programmer developsthe program gradually, by replacing angelic operators with pro-gressively more deterministic implementations.

In contrast to angelic nondeterminism that is purely a proofdevice, the ability to execute, test and observe angelic programsallows the programmer to refine programs and develop abstrac-tions, such as the parasitic stack, that seem difficult for humanswithout the computational power of the oracle.

References[1] J.-R. Abrial. The B Book: Assigning Programs to Meanings.

Cambridge Un. Press, Aug. 1996.[2] J.-R. Abrial. Event based sequential program development:

Application to constructing a pointer program. In FME, pages 51–74,2003.

[3] R.-J. Back and J. von Wright. Contracts, games, and refinement. Inf.Comput, 156(1-2):25–45, 2000.

[4] O. Celiku and J. von Wright. Implementing angelic nondeterminism.In APSEC, pages 176–185. IEEE Computer Society, 2003.

[5] E. M. Clarke, O. Grumberg, and D. A. Peled. Model Checking. TheMIT Press, Cambridge, Massachusetts, 1999.

[6] E. W. Dijkstra. A Discipline of Programming. Prentice Hall PTR,Upper Saddle River, NJ, USA, 1997.

[7] R. W. Floyd. Nondeterministic algorithms. Journal of the ACM,14(4):636–644, oct 1967.

[8] M. F. Frias, C. G. López Pombo, G. A. Baum, N. M. Aguirre, andT. S. E. Maibaum. Reasoning about static and dynamic propertiesin alloy: A purely relational approach. ACM Trans. Softw. Eng.Methodol., 14(4):478–526, 2005.

[9] T. A. Lau, P. Domingos, and D. S. Weld. Learning programs fromtraces using version space algebra. In K-CAP, pages 36–43, 2003.

[10] C. L. McMaster. An analysis of algorithms for the dutch national flagproblem. Commun. ACM, 21(10):842–846, 1978.

[11] C. Morgan. Programming from Specifications. 1998.[12] C. Morgan and T. Vickers, editors. On the Refinement Calculus.

Springer-Verlag, London, 1992.[13] M. Odersky and al. An overview of the scala programming language.

Technical Report IC/2004/64, EPFL Lausanne, Switzerland, 2004.[14] M. O. Rabin and D. S. Scott. Finite automata and their decision

problems. IBM J. Res. and Develop, 3:114–125, 1959.[15] H. Schorr and W. Waite. An efficient machine independent procedure

for garbage collection in various list structures. Communications ofthe ACM, 10(8):501–506, Aug. 1967.

[16] G. Smith and J. Derrick. Abstract specification in object-Z and CSP.In C. George and H. Miao, editors, Formal Methods and SoftwareEngineering, volume 2495 of Lecture Notes in Computer Science,pages 108–119. Springer, Nov. 2002.

[17] A. Solar-Lezama, G. Arnold, L. Tancau, R. Bodik, V. Saraswat, andS. Seshia. Sketching stencils. In PLDI ’07: Proceedings of the 2007ACM SIGPLAN conference on Programming language design andimplementation, pages 167–178, New York, NY, USA, 2007. ACM.

[18] A. Solar-Lezama, L. Tancau, R. Bodik, S. Seshia, and V. Saraswat.Combinatorial sketching for finite programs. SIGPLAN Not.,41(11):404–415, 2006.

[19] J. M. Spivey. Understanding Z: a specification language and itsformal semantics. Cambridge Un. Press, New York, NY, USA, 1988.

[20] N. Ward and I. J. Hayes. Applications of angelic nondeterminism.In P. A. Bailes, editor, Proc. 6th Australian Software EngineeringConference (ASWEC91), pages 391–404. Australian ComputerSociety, JUL 1991.

351