Program Analysis
Program Analysis
Summer 2019
Contents
1 Introduction 3
3 Program Semantics 7
3.1 Operational Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.1.1 W HILE: Big-step operational semantics . . . . . . . . . . . . . . . . . . . . 7
3.1.2 W HILE: Small-step operational semantics . . . . . . . . . . . . . . . . . . . 9
3.1.3 W HILE 3A DDR: Small-step semantics . . . . . . . . . . . . . . . . . . . . . 10
3.1.4 Derivations and provability . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2 Proof techniques using operational semantics . . . . . . . . . . . . . . . . . . . . . 11
6 Interprocedural Analysis 29
6.1 Default Assumptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.2 Annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6.3 Local vs. global variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6.4 Interprocedural Control Flow Graph . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6.5 Context Sensitive Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
6.6 Precision . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6.7 Termination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6.8 Approaches to Limiting Context-Sensitivity . . . . . . . . . . . . . . . . . . . . . . 34
1
7 Pointer Analysis 37
7.1 Motivation for Pointer Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
7.2 Andersen’s Points-To Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
7.2.1 Field-Insensitive Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7.2.2 Field-Sensitive Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7.3 Steensgaard’s Points-To Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
9 Symbolic Execution 52
9.1 Symbolic Execution Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
9.1.1 A Generalization of Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
9.1.2 History of Symbolic Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . 53
9.2 Symbolic Execution Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
9.3 Heap Manipulating Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
9.4 Symbolic Execution Implementation and Industrial Use . . . . . . . . . . . . . . . 55
10 Program Synthesis 56
10.1 Program Synthesis Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
10.2 Inductive Synthesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
10.2.1 SKETCH, CEGIS, and SyGuS . . . . . . . . . . . . . . . . . . . . . . . . . . 58
10.2.2 Oracle-guided synthesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
10.3 Oracle-guided Component-based Program Synthesis . . . . . . . . . . . . . . . . 59
12 Concolic Testing 67
12.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
12.2 Goals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
12.3 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
12.4 Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
12.5 Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2
Chapter 1
Introduction
Software is transforming the way that we live and work. We communicate with friends via
social media on smartphones, and use websites to buy what we need and to learn about any-
thing in the world. At work, software helps us organize our businesses, reach customers, and
distinguish ourselves from competitors.
Unfortunately, it is still challenging to produce high-quality software, and much of the
software we do use has bugs and security vulnerabilities. Recent examples of problems caused
by buggy software include uncontrollable accelleration in Toyota cars, personal information
stolen from Facebook by Cambridge Analytica, and a glitch in Nest smart thermostats left
many homes without heat. Just looking at one category of defect, software race conditions, we
observe problems ranging from power outages affecting millions of people in the US Northeast
in 2003 to deadly radiation overdoses from the Therac-25 radiation therapy machine.
Program analysis is all about analyzing software code to learn about its properties. Pro-
gram analyses can find bugs or security vulnerabilities like the ones mentioned above. It can
also be used to synthesize test cases for software, and even to automatically patch software.
For example, Facebook uses the Getafix tool to automatically produce patches for bugs found
by other analysis tools.1 Finally, program analysis is used in compiler optimizations in order
to make programs run faster.
This book covers both foundations and practical aspects of the automated analysis of pro-
grams, which is becoming increasingly critical to find software errors, assure program correct-
ness, and discover properties of code. We start by looking at how we can use mathematical
formalisms to reason about how programs execute, then examine how programs are repre-
sented within compilers and analysis tools. We study dataflow analyiss and the correspond-
ing theory of abstract interpretation, which captures the essence of a broad range of program
analyses and supports reasoning about their correctness. Building on this foundation, later
chapters will describe various kinds of dataflow analysis, pointer analysis, interprocedural
analysis, and symbolic execution.
In course assignments that go with this book, students will design and implement analy-
sis tools that find bugs and verify properties of software. Students will apply knowledge and
skills learned in the course to a capstone research project that involves designing, implement-
ing, and evaluating a novel program analysis.
Overall program analysis is an area with deep mathematical foundations that is also very
practically useful. I hope you will also find it to be fun!
1
See https://code.fb.com/developer-tools/getafix-how-facebook-tools-learn-to-fix-bugs-automatically/
3
Chapter 2
S statements
a arithmetic expressions (AExp)
x, y program variables (Vars)
n number literals
P boolean predicates (BExp)
The syntax of W HILE is shown below. Statements S can be an assignment x : a; a skip
statement, which does nothing;1 and if and while statements, with boolean predicates P
as conditions. Arithmetic expressions a include variables x, numbers n, and one of several
arithmetic operators (opa ). Predicates are represented by Boolean expressions that include
true, false, the negation of another Boolean expression, Boolean operators opb applied to other
Boolean expressions, and relational operators opr applied to arithmetic expressions.
1
Similar to a lone semicolon or open/close bracket in C or Java
4
2.2 W HILE 3A DDR: A Representation for Analysis
For analysis, the source-like definition of W HILE can sometimes prove inconvenient. For ex-
ample, W HILE has three separate syntactic forms—statements, arithmetic expressions, and
boolean predicates—and we would have to define the semantics and analysis of each sepa-
rately to reason about it. A simpler and more regular representation of programs will help
simplify certain of our formalisms.
As a starting point, we will eliminate recursive arithmetic and boolean expressions and
replace them with simple atomic statement forms, which are called instructions, after the as-
sembly language instructions that they resemble. For example, an assignment statement of the
form w x y z will be rewritten as a multiply instruction followed by an add instruction.
The multiply assigns to a temporary variable t1 , which is then used in the subsequent add:
t1 x y
w t1 z
As the translation from expressions to instructions suggests, program analysis is typically
studied using a representation of programs that is not only simpler, but also lower-level than
the source (W HILE, in this instance) language. Many Java analyses are actually conducted on
byte code, for example. Typically, high-level languages come with features that are numerous
and complex, but can be reduced into a smaller set of simpler primitives. Working at the lower
level of abstraction thus also supports simplicity in the compiler.
Control flow constructs such as if and while are similarly translated into simpler jump
and conditional branch constructs that jump to a particular (numbered) instruction. For exam-
ple, a statement of the form if P then S1 else S2 would be translated into:
1: if P then goto 4
2: S2
3: goto 5
4: S1
Exercise 1. How would you translate a W HILE statement of the form while P do S?
This form of code is often called 3-address code, because every instruction has at most
two source operands and one result operand. We now define the syntax for 3-address code
produced from the W HILE language, which we will call W HILE 3A DDR. This language consists
of a set of simple instructions that load a constant into a variable, copy from one variable to
another, compute the value of a variable from two others, or jump (possibly conditionally) to
a new address n. A program P is just a map from addresses to instructions:2
5
2.3 Extensions
The languages described above are sufficient to introduce the fundamental concepts of pro-
gram analysis in this course. However, we will eventually examine various extensions to
W HILE and W HILE 3A DDR, so that we can understand how more complicated constructs in
real languages can be analyzed. Some of these extensions to W HILE 3A DDR will include:
I :: . . .
| x : f pyq function call
| return x return
| x : y.mpzq method call
| x : &p address-of operator
| x : p pointer dereference
| p : x pointer assignment
| x : y.f field read
| x.f : y field assignment
We will not give semantics to these extensions now, but it is useful to be aware of them as
you will see intermediate code like this in practical analysis frameworks.
6
Chapter 3
Program Semantics
E P Var Ñ Z
Here E denotes a particular program state. The meaning of an expression with a variable like
x 5 involves “looking up” the x’s value in the associated E, and substituting it in. Given a
state, we can write a judgement as follows:
xa, E y ó n
This means that given program state E, the expression e evaluates to n. This formulation is
called big-step operational semantics; the ó judgement relates an expression and its “mean-
ing.”1 We then build up the meaning of more complex expressions using rules of inference (also
called derivation or evaluation rules). An inference rule is made up of a set of judgments above
the line, known as premises, and a judgment below the line, known as the conclusion. The
meaning of an inference rule is that the conclusion holds if all of the premises hold:
1
Note that I have chosen ó because it is a common notational convention; it’s not otherwise special. This is true
for many notational choices in formal specification.
7
premise1 premise2 . . . premisen
conclusion
An inference rule with no premises is an axiom, which is always true. For example, integers
always evaluate to themselves, and the meaning of a variable is its stored value in the state:
xa1, E y ó n1 xa2, E y ó n2
xa1 a2, E y ó n1 n2 big-add
But, how does the value of x come to be “stored” in E? For that, we must consider W HILE
Statements. Unlike expressions, statements have no direct result. However, they can have side
effects. That is to say: the “result” or meaning of a Statement is a new state. The judgement ó as
applied to statements and states therefore looks like:
xS, E y ó E 1
This allows us to write inference rules for statements, bearing in mind that their meaning is
not an integer, but a new state. The meaning of skip, for example, is an unchanged state:
xskip, E y ó E big-skip
xS1, E y ó E 1 xS2, E 1y ó E 2
xS1; S2, E y ó E 2 big-seq
The if statement involves two rules, one for if the boolean predicate evaluates to true
(rules for boolean expressions not shown), and one for if it evaluates to false. I’ll show you
just the first one for demonstration:
This brings us to assignments, which produce a new state in which the variable being assigned
to is mapped to the value from the right-hand side. We write this with the notation E rx ÞÑ ns,
which can be read “a new state that is the same as E except that x is mapped to n.”
xa, E y ó n
xx : a, E y ó E rx ÞÑ ns big-assign
Note that the update to the state is modeled functionally; the variable E still refers to the
old state, while E rx ÞÑ ns is the new state represented as a mathematical map.
Fully specifying the semantics of a language requires a judgement rule like this for every
language construct. These notes only include a subset for W HILE, for brevity.
8
3.1.2 W HILE: Small-step operational semantics
Big-step operational semantics has its uses. Among other nice features, it directly suggests a
simple interpreter implementation for a given language. However, it is difficult to talk about
a statement or program whose evaluation does not terminate. Nor does it give us any way to
talk about intermediate states (so modeling multiple threads of control is out).
Sometimes it is instead useful to define a small-step operational semantics, which specifies
program execution one step at a time. We refer to the pair of a statement and a state (xS, E y) as
a configuration. Whereas big step semantics specifies program meaning as a function between a
configuration and a new state, small step models it as a step from one configuration to another.
You can think of small-step semantics as a set of rules that we repeatedly apply to configu-
rations until we reach a final configuration for the language (xskip, E y, in this case) if ever.2 We
write this new judgement using a slightly different arrow: Ñ. xS, E y Ñ xS 1 , E 1 y indicates one
step of execution; xS, E y Ñ xS 1 , E 1 y indicates zero or more steps of execution. We formally
define multiple execution steps as follows:
xS, E y Ñ xS 1, E 1y xS 1, E 1y Ñ xS 2, E 2y
xS, E y Ñ xS, E y multi-reflexive
xS, E y Ñ xS 2, E 2y multi-inductive
To be complete, we should also define auxiliary small-step operators Ña and Ñb for arith-
metic and boolean expressions, respectively; only the operator for statements results in an
updated state (as in big step). The types of these judgements are thus:
Ñ : pStmt E q Ñ pStmt E q
Ña : pAexp E q Ñ Aexp
Ñb : pBexp E q Ñ Bexp
We can now again write the semantics of a W HILE program as new rules of inference. Some
rules look very similar to the big-step rules, just with a different arrow. For example, consider
variables:
xP, E y Ñb P 1
xif P then S1 else S2, E y Ñ xif P 1 then S1 else S2, E y small-if-congruence
9
3.1.3 W HILE 3A DDR: Small-step semantics
The ideas behind big- and small-step operational semantics are consistent across languages,
but the way they are written can vary based on what is notationally convenient for a particular
language or analysis. W HILE 3A DDR is slightly different from W HILE, so beyond requiring
different rules for its different constructs, it makes sense to modify our small-step notation a
bit for defining the meaning of a W HILE 3A DDR program.
First, let’s revisit the configuration to account for the slightly different meaning of a
W HILE 3A DDR program. As before, the configuration must include the state, which we still
call E, mapping variables to values. However, a well-formed, terminating W HILE program
was effectively a single statement that can be iteratively reduced to skip; a W HILE 3A DDR
program, on the other hand, is a mapping from natural numbers to program instructions. So,
instead of a statement that is being reduced in steps, the W HILE 3A DDR c must includes a
program counter n, representing the next instruction to be executed.
Thus, a configuration c of the abstract machine for W HILE 3A DDR must include the stored
program P (which we will generally treat implicitly), the state environment E, and the current
program counter n representing the next instruction to be executed (c P E N). The abstract
machine executes one step at a time, executing the instruction that the program counter points
to, and updating the program counter and environment according to the semantics of that
instruction.
This adds a tiny bit of complexity to the inference rules, because they must explicitly con-
sider the mapping between line number/labels and program instructions. We represent exe-
cution of the abstract machine via a judgment of the form P $ xE, ny ; xE 1 , n1 y The judgment
reads: “When executing the program P , executing instruction n in the state E steps to a new
state E 1 and program counter n1 .”3 To see this in action, consider a simple inference rule defin-
ing the semantics of the constant assignment instruction:
P rns x : m
P $ xE, ny ; xE rx ÞÑ ms, n 1y
step-const
This states that in the case where the nth instruction of the program P (looked up using
P rns) is a constant assignment x : m, the abstract machine takes a step to a state in which the
state E is updated to map x to the constant m, written as E rx ÞÑ ms, and the program counter
now points to the instruction at the following address n 1. We similarly define the remaining
rules:
P rns x : y
P $ xE, ny ; xE rx ÞÑ E ry ss, n 1y
step-copy
P rns x : y op z E ry s op E rz s m
P $ xE, ny ; xE rx ÞÑ ms, n 1y
step-arith
P rns goto m
P $ xE, ny ; xE, my
step-goto
3
I could have used the same Ñ I did above instead of ;, but I don’t want you to mix them up.
10
3.1.4 Derivations and provability
Among other things, we can use operational semantics to prove that concrete program expres-
sions will evaluate to particular values. We do this by chaining together rules of inference
(which simply list the hypotheses necessary to arrive at a conclusion) into derivations, which
interlock instances of rules of inference to reach particular conclusions. For example:
a1 a1 a2
a1 a1 a2
a2 a1 a2
a2 a1 a2
. . . etc., for all arithmetic operators opa
To prove that a property P holds for all arithmetic expressions in W HILE (or, @a P
Aexp.P paq), we must show P holds for both the base cases and the inductive cases. a is a
4
Mathematical induction as a special case arises when is simply the predecessor relation (px, x 1q|x P N).
11
base case if there is no a1 such that a1 a; a is an inductive case if Da1 . a1 a. There is thus one
proof case per form of the expression. For Aexp, the base cases are:
$ @n P Z . P pnq
$ @x P Vars . P pxq
And the inductive cases:
Example. Let Lpaq be the number of literals and variable occurrences in some expression a
and Opaq be the number of operators in a. Prove by induction on the structure of a that @a P
Aexp . Lpaq Opaq 1:
Base cases:
• Case a n. Lpaq 1 and Opaq 0
• Case a x. Lpaq 1 and Opaq 0
Inductive case 1: Case a a1 a2
• By definition, Lpaq Lpa1 q Lpa2 q and Opaq Opa1 q Opa2 q 1.
• By the induction hypothesis, Lpa1 q Opa1 q 1 and Lpa2 q Opa2 q 1.
• Thus, Lpaq Opa1 q Opa2 q 2 Opaq 1.
The other arithmetic operators follow the same logic.
Other proofs for the expression sublanguages of W HILE can be similarly conducted. For
example, we could prove that the small-step and big-step semantics will obtain equivalent
results on expressions:
We can’t prove the third statement with structural induction on the language syntax be-
cause the evaluation of statements (like while) does not depend only on the evaluation of its
subexpressions.
Fortunately, there is another way. Recall that the operational semantics assign meaning
to programs by providing rules of inference that allow us to prove judgements by making
derivations. Derivation trees (like the expression trees we discussed above) are also defined
inductively, and are built of sub-derivations. Because they have structure, we can again use
structural induction, but here, on the structure of derivations.
12
Instead of assuming (and reasoning about) some statement S, we instead assume a deriva-
tion D :: xS, E y ó E 1 and induct on the structure of that derivation (we define D :: Judgement
to mean “D is the derivation that proves judgement.” e.g., D :: xx 1, E y ó 2). That is, to prove
that property P holds for a statement, we will prove that P holds for all possible derivations
of that statement. Such a proof consists of the following steps:
Base cases: show that P holds for each atomic derivation rule with no premises (of the form
S).
Inductive cases: For each derivation rule of the form
H1 ...Hn
S
By the induction hypothesis, P holds for Hi , where i 1 . . . n. We then have to prove that the
property is preserved by the derivation using the given rule of inference.
A key technique for induction on derivations is inversion. Because the number of forms
of rules of inference is finite, we can tell which inference rules might have been used last in
the derivation. For example, given D :: xx : 55, Ei y ó E, we know (by inversion) that
the assignment rule of inference must be the last rule used in D (because no other rules of
inference involve an assignment statement in their concluding judgment). Similarly, if D ::
xwhile P do S, Eiy ó E, then (by inversion) the last rule used in D was either the while-true
rule or the while-false rule.
Given those preliminaries, to prove that the evaluation of statements is deterministic (equa-
tion (3) above), pick arbitrary S, E, E 1 , and D :: xS, E y ó E 1
Proof: by induction of the structure of the derivation D, which we define D :: xS, E y ó E 1 .
Base case: the one rule with no premises, skip:
D :: xskip, E y ó E
By inversion, the last rule used in D1 (which, again, produced E 2 ) must also have been the
rule for skip. By the structure of the skip rule, we know E 2 E.
Inductive cases: We need to show that the property holds when the last rule used in D was
each of the possible non-skip W HILE commands. I will show you one representative case; the
rest are left as an exercise. If the last rule used was the while-true statement:
13
Chapter 4
σ P Var Ñ L
L represents the set of abstract values we are interested in tracking in the analysis. This
varies from one analysis to another. For example, consider a zero analysis, which tracks whether
each variable is zero or not at each program point (Thought Question: Why would this be
useful?). For this analysis, we define L to be the set tZ, N, Ju. The abstract value Z represents
the value 0, N represents all nonzero values. J is pronounced “top”, and we define it more
concretely later it in these notes; we use it as a question mark, for the situations when we do
not know whether a variable is zero or not, due to imprecision in the analysis.
Conceptually, each abstract value represents a set of one or more concrete values that may
occur when a program executes. We define an abstraction function α that maps each possible
concrete value of interest to an abstract value:
α:ZÑL
For zero analysis, we define α so that 0 maps to Z and all other integers map to N :
αZ p0q Z
αZ pnq N where n 0
The core of any program analysis is how individual instructions in the program are ana-
lyzed and affect the analysis state σ at each program point. We define this using flow functions
that map the dataflow information at the program point immediately before an instruction to
the dataflow information after that instruction. A flow function should represent the semantics
of the instruction, but abstractly, in terms of the abstract values tracked by the analysis. We
will link semantics to the flow function precisely when we talk about correctness of dataflow
analysis. For now, to approach the idea by example, we define the flow functions fZ for zero
analysis on W HILE 3A DDR as follows:
14
fZ vx : 0wpσ q σrx ÞÑ Z s (4.1)
fZ vx : nwpσ q σrx ÞÑ N s where n 0 (4.2)
fZ vx : y wpσ q σrx ÞÑ σpyqs (4.3)
fZ vx : y op z wpσ q σrx ÞÑ Js (4.4)
fZ vgoto nwpσ q σ (4.5)
fZ vif x 0 goto nwpσ q σ (4.6)
In the notation, the form of the instruction is an implicit argument to the function, which is
followed by the explicit dataflow information argument, in the form fZ vI wpσ q. (1) and (2) are
for assignment to a constant. If we assign 0 to a variable x, then we should update the input
dataflow information σ so that x maps to the abstract value Z. The notation σ rx ÞÑ Z s denotes
dataflow information that is identical to σ except that the value in the mapping for x refers to
Z. Flow function (3) is for copies from a variable y to another variable x: we look up y in σ,
written σ py q, and update σ so that x maps to the same abstract value as y.
We start with a generic flow function for arithmetic instructions (4). Arithmetic can produce
either a zero or a nonzero value, so we use the abstract value J to represent our uncertainty.
More precise flow functions are available based on certain instructions or operands. For exam-
ple, if the instruction is subtraction and the operands are the same, the result will definitely be
zero. Or, if the instruction is addition, and the analysis information tells us that one operand
is zero, then the addition is really a copy and we can use a flow function similar to the copy
instruction above. These examples could be written as follows (we would still need the generic
case above for instructions that do not fit such special cases):
fZ vx : y y wpσ q σrx ÞÑ Z s
fZ vx : y z wpσ q σ rx ÞÑ σ py qs where σ pz q Z
Exercise 1. Define another flow function for some arithmetic instruction and certain conditions
where you can also provide a more precise result than J.
The flow function for branches ((5) and (6)) is trivial: branches do not change the state of the
machine other than to change the program counter, and thus the analysis result is unaffected.
However, we can provide a better flow function for conditional branches if we distinguish
the analysis information produced when the branch is taken or not taken. To do this, we
extend our notation once more in defining flow functions for branches, using a subscript to the
instruction to indicate whether we are specifying the dataflow information for the case where
the condition is true (T ) or when it is false (F ). For example, to define the flow function for
the true condition when testing a variable for equality with zero, we use the notation fZ vif x
0 goto nwT pσ q. In this case we know that x is zero so we can update σ with the Z lattice value.
Conversely, in the false condition we know that x is nonzero:
Exercise 2. Define a flow function for a conditional branch testing whether a variable x 0.
15
4.2 Running a dataflow analysis
The point of developing a dataflow analysis is to compute information about possible program
states at each point in a program. For example, for of zero analysis, whenever we divide some
expression by a variable x, we might like to know whether x must be zero (the abstract value
Z) or may be zero (represented by J) so that we can warn the developer.
x : 0
x y z
1:
y : 1
1 Z
2:
z : y
2 Z N
3:
y : z x
3 Z N N
4:
x : y z
4 Z N N
5:
5 J N N
We simulate running the program in the analysis, using the flow function to compute,
for each instruction in turn, the dataflow analysis information after the instruction from the
information we had before the instruction. For such simple code, it is easy to track the analysis
information using a table with a column for each program variable and a row for each program
point (right, above). The information in a cell tells us the abstract value of the column’s variable
immediately after the instruction at that line (corresponding the the program points labeled
with circles in the CFG).
Notice that the analysis is imprecise at the end with respect to the value of x. We were
able to keep track of which values are zero and nonzero quite well through instruction 4,
using (in the last case) the flow function that knows that adding a variable known to be
zero is equivalent to a copy. However, at instruction 5, the analysis does not know that y
and z are equal, and so it cannot determine whether x will be zero. Because the analysis is
not tracking the exact values of variables, but rather approximations, it will inevitably be
imprecise in certain situations. However, in practice, well-designed approximations can often
allow dataflow analysis to compute quite useful information.
16
x y z
1: if x 0 goto 4
2: y : 0 1 ZT , NF
3: goto 6 2 N Z
4: y : 1 3 N Z
5: x : 1 4
6: z : y 5
6 N Z Z
In the table above, the entry for x on line 1 indicates the different abstract values produced
for the true and false conditions of the branch. We use the false condition (x is nonzero) in
analyzing instruction 2. Execution proceeds through instruction 3, at which point we jump to
instruction 6. We have not yet analyzed a path through lines 4 and 5.
Turning to that alternative path, we can start by analyzing instructions 4 and 5 as if we had
taken the true branch at instruction 1:
x y z
1 ZT , NF
2 N Z
3 N Z
4 Z N
5 N N
6 N Z Z note: incorrect!
17
x y z
1 ZT , NF
2 N Z
3 N Z
4 Z N
5 N N
6 N J J corrected
4.2.3 Join
We generalize the procedure of combining analysis results along multiple paths by using a join
operation, \. When taking two abstract values l1 , l2 P L, the result of l1 \ l2 is an abstract value
lj that generalizes both l1 and l2 .
To precisely define what “generalizes” means, we define a partial order over abstract
values, and say that l1 and l2 are at least as precise as lj , written l1 lj . Recall that a partial
order is any relation that is:
• reflexive: @l : l l
• transitive: @l1 , l2 , l3 : l1 l2 ^ l2 l3 ñ l1 l3
• anti-symmetric: @l1 , l2 : l1 l2 ^ l2 l1 ñ l1 l2
A set of values L that is equipped with a partial order , and for which the least upper
bound of any two values in that ordering l1 \ l2 is unique and is also in L, is called a join-
semilattice. Any join-semilattice has a maximal element J (pronounced “top”). We require
that the abstract values used in dataflow analyses form a join-semilattice. We will use the
term lattice for short; as we will see below, this is the correct terminology for most dataflow
analyses anyway. For zero analysis, we define the partial order with Z J and N J, where
Z \ N J.
We have now introduced and considered all the elements necessary to define a dataflow
analysis:
• a lattice pL, q
• an abstraction function α
• initial dataflow analysis assumptions σ0
• a flow function f
Note that the theory of lattices answers a side question that comes up when we begin
analyzing the first program instruction: what should we assume about the value of input
variables (like x on program entry)? If we do not know anything about the value x can be,
a good choice is to assume it can be anything; That is, in the initial environment σ0 , input
variables like x are mapped to J.
18
gram paths. Despite this, we would like to analyze looping programs in bounded time. Let us
examine how through the following simple looping example:1
x : 10
x y z
1:
y : 0
1 N
2:
z : 0
2 N Z
3:
if x 0 goto 8
3 N Z Z
4:
y : 1
4 ZT , NF Z Z
5:
x : x 1
5 N N Z
6:
6 J N Z
7:
8:
goto 4
x : y
7 J N Z
8
The right-hand side above shows the straightforward straight-line analysis of the path that
runs the loop once. We must now re-analyze instruction 4. This should not be surprising;
it is analogous to the one we encountered earlier, merging paths after an if instruction. To
determine the analysis information at instruction 4, we join the dataflow analysis information
flowing in from instruction 3 with the dataflow analysis information flowing in from instruc-
tion 7. For x we have N \ J J. For y we have Z \ N J. For z we have Z \ Z Z. The
information for instruction 4 is therefore unchanged, except that for y we now have J.
We can now choose between two paths once again: staying within the loop, or exiting out
to instruction 8. We will choose (arbitrarily, for now) to stay within the loop, and consider in-
struction 5. This is our second visit to instruction 5, and we have new information to consider:
since we have gone through the loop, the assignment y : 1 has been executed, and we have
to assume that y may be nonzero coming into instruction 5. This is accounted for by the latest
update to instruction 4’s analysis information, in which y is mapped to J. Thus the informa-
tion for instruction 4 describes both possible paths. We must update the analysis information
for instruction 5 so it does so as well. In this case, however, since the instruction assigns 1 to
y, we still know that y is nonzero after it executes. In fact, analyzing the instruction again with
the updated input data does not change the analysis results for this instruction.
A quick check shows that going through the remaining instructions in the loop, and even
coming back to instruction 4, the analysis information will not change. That is because the
flow functions are deterministic: given the same input analysis information and the same in-
struction, they will produce the same output analysis information. If we analyze instruction
6, for example, the input analysis information from instruction 5 is the same input analysis
information we used when analyzing instruction 6 the last time around. Thus, instruction 6’s
output information will not change, and so instruction 7’s input information will not change,
and so on. No matter which instruction we run the analysis on, anywhere in the loop (and in
fact before the loop), the analysis information will not change.
We say that the dataflow analysis has reached a fixed point.2 In mathematics, a fixed point
1
I provide the CFG for reference but omit the annotations in the interest of a cleaner diagram.
2
Sometimes abbreviated in one word as fixpoint.
19
of a function is a data value v that is mapped to itself by the function, i.e. f pv q v. In this
analysis, the mathematical function is the flow function, and the fixed point is a tuple of the
dataflow analysis values at each program point. If we invoke the flow function on the fixed
point, the analysis results do not change (we get the same fixed point back).
Once we have reached a fixed point of the function for this loop, it is clear that further
analysis of the loop will not be useful. Therefore, we will proceed to analyze statement 8. The
final analysis results are as follows:
x y z
1 N
2 N Z
3 N Z Z
4 ZT , NF J Z updated
5 N N Z already at fixed point
6 J N Z already at fixed point
7 J N Z already at fixed point
8 Z J Z
Quickly simulating a run of the program program shows that these results correctly ap-
proximate actual execution. The uncertainty in the value of x at instructions 6 and 7 is real: x
is nonzero after these instructions, except the last time through the loop, when it is zero. The
uncertainty in the value of y at the end shows imprecision in the analysis: this loop always
executes at least once, so y will be nonzero. However, the analysis (as currently formulated)
cannot tell this for certain, so it reports that it cannot tell if y is zero or not. This is safe—it is
always correct to say the analysis is uncertain—but not as precise as would be ideal.
The benefit of analysis, however, is that we can gain correct information about all possible
executions of the program with only a finite amount of work. In our example, we only had
to analyze the loop statements at most twice each before reaching a fixed point. This is a
significant improvement over the actual program execution, which runs the loop 10 times. We
sacrificed precision in exchange for coverage of all possible executions, a classic tradeoff in
static analysis.
How can we be confident that the results of the analysis are correct, besides simulating
every possible run of a (possibly very complex) program? The intuition behind correctness
is the invariant that at each program point, the analysis results approximate all the possible
program values that could exist at that point. If the analysis information at the beginning of
the program correctly approximates the program arguments, then the invariant is true at the
beginning of program execution. One can then make an inductive argument that the invariant
is preserved. In particular, when the program executes an instruction, the instruction modifies
the program’s state. As long as the flow functions account for every possible way that instruc-
tion can modify state, then at the analysis fixed point they will have correctly approximated
actual program execution. We will make this argument more precise in a future lecture.
20
have to worry about following a specific path during analysis. However, for instruction 4, this
requires a dataflow value from instruction 7, even if instruction 7 has not yet been analyzed.
We could do this if we had a dataflow value that is always ignored when it is joined with
any other dataflow value. In other words, we need a abstract dataflow value K (pronounced
“bottom”) such that K \ l l.
K plays a dual role to the value J: it sits at the bottom of the dataflow value lattice. For
all l, we have the identity l J and correspondingly K l. There is an greatest lower bound
operator meet, [, which is dual to \. The meet of all dataflow values is K.
A set of values L that is equipped with a partial order , and for which both least upper
bounds \ and greatest lower bounds [ exist in L and are unique, is called a complete lattice.
The theory of K and complete lattices provides an elegant solution to the problem men-
tioned above. We can initialize σ at every instruction in the program, except at entry, to K,
indicating that the instruction there has not yet been analyzed. We can then always merge all
input values to a node, whether or not the sources of those inputs have been analysed, because
we know that any K values from unanalyzed sources will simply be ignored by the join oper-
ator \, and that if the dataflow value for that variable will change, we will get to it before the
analysis is completed.
21
discuss in a future lecture) then each time the flow function runs on a given instruction, either
the results do not change, or they get become more approximate (i.e. they are higher in the
lattice). Later runs of the flow function consider more possible paths through the program
and therefore produce a more approximate result which considers all these possibilities. If the
lattice is of finite height—meaning there are at most a finite number of steps from any place in
the lattice going up towards the J value—then this process must terminate eventually. More
concretely: once an abstract value is computed to be J, it will stay J no matter how many
times the analysis is run. The abstraction only flows in one direction.
Although the simple algorithm above always terminates and results in the correct answer,
it is still not always the most efficient. Typically, for example, it is beneficial to analyze the
program instructions in order, so that results from earlier instructions can be used to update the
results of later instructions. It is also useful to keep track of a list of instructions for which there
has been a change since the instruction was last analyzed in the result dataflow information
of some predecessor. Only those instructions need be analyzed; reanalyzing other instructions
is useless since their input has not changed. Kildall captured this intuition with his worklist
algorithm, described in pseudocode as:
for Instruction i in program
input[i] = K
input[firstInstruction] = initialDataflowInformation
worklist = { firstInstruction }
22
to a fixed point, then have to redo that work when dataflow information from an earlier loop
changes.
Within each loop, the instructions should be processed in reverse postorder, the reverse
of the order in which each node is last visited when traversing a tree. Consider the example
from Section 4.2.2 above, in which instruction 1 is an if test, instructions 2-3 are the then
branch, instructions 4-5 are the else branch, and instruction 6 comes after the if statement.
A tree traversal might go as follows: 1, 2, 3, 6, 3 (again), 2 (again), 1 (again), 4, 5, 4 (again),
1 (again). Some instructions in the tree are visited multiple times: once going down, once
between visiting the children, and once coming up. The postorder, or order of the last visits to
each node, is 6, 3, 2, 5, 4, 1. The reverse postorder is the reverse of this: 1, 4, 5, 2, 3, 6. Now we
can see why reverse postorder works well: we explore both branches of the if statement (4-5
and 2-3) before we explore node 6. This ensures that we do not have to reanalyze node 6 after
one of its inputs changes.
Although analyzing code using the strongly-connected component and reverse postorder
heuristics improves performance substantially in practice, it does not change the worst-case
performance results described above.
23
Chapter 5
σ P Var Ñ LCP
σ1 lif t σ2 iff @x P Var : σ1 pxq σ2 pxq
σ1 \lif t σ2 tx ÞÑ σ1 pxq \ σ2 pxq | x P Varu
Jlif t tx ÞÑ J | x P Varu
Klif t tx ÞÑ K | x P Varu
We can likewise define an abstraction function for constant propagation, as well as a lifted
version that accepts an environment E mapping variables to concrete values. We also define
the initial analysis information to conservatively assume that initial variable values are un-
known. Note that in a language that initializes all variables to zero, we could make more
precise initial dataflow assumptions, such as tx ÞÑ 0 | x P Varu:
αCP pnq n
αlif t pE q tx ÞÑ αCP pE pxqq | x P Varu
σ0 Jlif t
We can now define flow functions for constant propagation:
24
fCP vx : nwpσ q σrx ÞÑ αCP pnqs
fCP vx : y wpσ q σrx ÞÑ σpyqs
fCP vx : y op z wpσ q σrx ÞÑ σpyq oplif t σpzqs
where n oplif t m n op m
and n oplif t K K (and symmetric)
1: y : x
2: z : 1
3: if y 0 goto 7
4: z : z y
5: y : y 1
6: goto 3
7: y : 0
In this example, definitions 1 and 5 reach the use of y at 4.
25
What should be for reaching definitions? The intuition is that our analysis is more precise
the smaller the set of definitions it computes at a given program point. This is because we want
to know, as precisely as possible, where the values at a program point came from. So should
be the subset relation : a subset is more precise than its superset. This naturally implies that
\ should be union, and that J and K should be the universal set DEFS and the empty set H,
respectively.
In summary, we can formally define our lattice and initial dataflow information as follows:
σ P P DEFS
σ1 σ2 iff σ1 σ2
σ1 \ σ2 σ1 Y σ2
J DEFS
K H
σ0 H
Instead of using the empty set for σ0 , we could use an artificial reaching definition for each
program variable (e.g. x0 as an artificial reaching definition for x) to denote that the variable is
either uninitialized, or was passed in as a parameter. This is convenient if it is useful to track
whether a variable might be uninitialized at a use, or if we want to consider a parameter to be
a definition. We could write this formally as σ0 tx0 | x P Varsu
We will now define flow functions for reaching definitions. Notationally, we will write xn
to denote a definition of the variable x at the program instruction numbered n. Since our lattice
is a set, we can reason about changes to it in terms of elements that are added (called GEN)
and elements that are removed (called KILL) for each statement. This GEN/KILL pattern is
common to many dataflow analyses. The flow functions can be formally defined as follows:
26
1: y : x
2: z : 1
3: if y 0 goto 7
4: z : z y
5: y : y 1
6: goto 3
7: y : 0
In this example, after instruction 1, y is live, but x and z are not. Live variables analysis
typically requires knowing what variable holds the main result(s) computed by the program.
In the program above, suppose z is the result of the program. Then at the end of the program,
only z is live.
Live variable analysis was originally developed for optimization purposes: if a variable is
not live after it is defined, we can remove the definition instruction. For example, instruction 7
in the code above could be optimized away, under our assumption that z is the only program
result of interest.
We must be careful of the side effects of a statement, of course. Assigning a variable that is
no longer live to null could have the beneficial side effect of allowing the garbage collector to
collect memory that is no longer reachable—unless the GC itself takes into consideration which
variables are live. Sometimes warning the user that an assignment has no effect can be useful
for software engineering purposes, even if the assignment cannot safely be optimized away.
For example, eBay found that FindBugs’s analysis detecting assignments to dead variables was
useful for identifying unnecessary database calls.1
For live variable analysis, we will use a set lattice to track the set of live variables at each
program point. The lattice is similar to that for reaching definitions:
σ P P Var
σ1 σ2 iff σ1 σ2
σ1 \ σ2 σ1 Y σ2
J Var
K H
What is the initial dataflow information? This is a tricky question. To determine the vari-
ables that are live at the start of the program, we must reason about how the program will
execute...i.e. we must run the live variables analysis itself! There’s no obvious assumption we
can make about this. On the other hand, it is quite clear which variables are live at the end of
the program: just the variable(s) holding the program result.
Consider how we might use this information to compute other live variables. Suppose the
last statement in the program assigns the program result z, computing it based on some other
variable x. Intuitively, that statement should make x live immediately above that statement, as
it is needed to compute the program result z—but z should now no longer be live. We can use
similar logic for the second-to-last statement, and so on. In fact, we can see that live variable
analysis is a backwards analysis: we start with dataflow information at the end of the program
and use flow functions to compute dataflow information at earlier statements.
Thus, for our “initial” dataflow information—and note that “initial” means the beginning
of the program analysis, but the end of the program—we have:
27
KILLLV vI w tx | I defines xu
GENLV vI w tx | I uses xu
We would compute dataflow analysis information for the program shown above as follows.
Note that we iterate over the program backwords, i.e. reversing control flow edges between
instructions. For each instruction, the corresponding row in our table will hold the information
after we have applied the flow function—that is, the variables that are live immediately before
the statement executes:
stmt worklist live
end 7 tz u
7 3 tz u
3 6,2 tz, yu
6 5,2 tz, yu
5 4,2 tz, yu
4 3,2 tz, yu
3 2 tz, yu
2 1 ty u
1 H tx u
28
Chapter 6
Interprocedural Analysis
Consider an extension of W HILE 3A DDR that includes functions. We thus add a new syntactic
category F (for functions), and two new instruction forms (function call and return), as follows:
f vx : g py qwpσ q
σrx ÞÑ Lr s perror if σpyq Laq
f vreturn xwpσ q σ perror if σpxq Lr q
We can apply zero analysis to the following function, using La Lr J:
29
1 : fun divByXpxq : int
2: y : 10{x
3: return y
4 : fun mainpq : void
5: z : 5
6: w : divByXpz q
The results are sound, but imprecise. We can avoid the false positive by using a more
optimistic assumption La Lr N Z. But then we get a problem with the following program:
Now what?
6.2 Annotations
An alternative approach uses annotations. This allows us to choose different argument and
result assumptions for different procedures. Flow functions might look like:
f vx : g py qwpσ q
σrx ÞÑ annotvgw.rs perror if σpyq annotvgw.aq
f vreturn xwpσ q σ perror if σpxq annotvgw.rq
Now we can verify that both of the above programs are safe, given the proper annotations. We
will see other example analysis approaches that use annotations later in the semester, though
historically, programmer buy-in remains a challenge in practice.
f vx : g py qwpσ q
σrx ÞÑ Lr srz ÞÑ Lg | z P Globalss
perror if σpyq La _ @z P Globals : σpzq Lg q
f vreturn xwpσ q σ
perror if σpxq Lr _ @z P Globals : σpzq Lg q
The annotation approach can also be extended in a natural way to handle global variables.
30
• We add additional edges to the control flow graph. For every call to function g, we add
an edge from the call site to the first instruction of g, and from every return statement of
g to the instruction following that call.
• When analyzing the first statement of a procedure, we generally gather analysis infor-
mation from each predecessor as usual. However, we take out all dataflow information
related to local variables in the callers. Furthermore, we add dataflow information for
parameters in the callee, initializing their dataflow values according to the actual argu-
ments passed in at each call site.
Now the examples described above can be successfully analyzed. However, other pro-
grams still cause problems:
31
Things become more challenging in the presence of recursive functions, or more gener-
ally mutual recursion. Let us consider context-sensitive interprocedural constant propagation
analysis of a factorial function called by main. We are not focused on the intraprocedural part
of the analysis, so we will just show the function in the form of Java or C source code:
int fact(int x) {
if (x == 1)
return 1;
else
return x * fact(x-1);
}
void main() {
int y = fact(2);
int z = fact(3);
int w = fact(getInputFromUser());
}
We can analyze the first two calls to fact within main in a straightforward way, and in fact
if we cache the results of analyzing fact(2) we can reuse this when analyzing the recursive call
inside fact(3).
For the third call to fact, the argument is determined at runtime and so constant propa-
gation uses J for the calling context. In this case the recursive call to fact() also has J as the
calling context. But we cannot look up the result in the cache yet as analysis of fact() with J
has not completed. A naive approach would attempt to analyze fact() with J again, and would
therefore not terminate.
We can solve the problem by applying the same idea as in intraprocedural analysis. The
recursive call is a kind of a loop. We can make the initial assumption that the result of the
recursive call is K, which is conceptually equivalent to information coming from the back edge
of a loop. When we discover the result is a higher point in the lattice then K, we reanalyze the
calling context (and recursively, all calling contexts that depend on it). The algorithm to do so
can be expressed as follows:
type Context
val f n : F unction the function being called
val input : σ input for this set of calls
type Summary the input/output summary for a context
val input : σ
val output : σ
val worklist : SetrContexts contexts we must revisit due to updated analysis information
val analyzing : Stack rContexts the contexts we are currently analyzing
val results : M aprContext, Summary s the analysis results
val callers : M aprContext, SetrContextss the call graph - used for change propagation
32
function A NALYZE P ROGRAM starting point for interprocedural analysis
worklist Ð tContextpmain, Jqu
resultsrContextpmain, Jqs.input Ð J
while N OT E MPTY(worklist) do
ctx Ð R EMOVE(worklist)
A NALYZE(ctx, resultsrctxs.input)
end while
end function
function A NALYZE(ctx, σi )
σo Ð resultsrctxs.output
P USH(analyzing, ctx)
σo1 ÐI NTRAPROCEDURAL(ctx, σi )
P OP(analyzing)
if σo1 σo then
resultsrctxs Ð Summary pσi , σo \ σo1 q
for c P callersrctxs do
A DD(worklist, c)
end for
end if
return σo1
end function
33
The following example shows that the algorithm generalizes naturally to the case of mutu-
ally recursive functions:
bar() { if (...) return 2 else return foo() }
foo() { if (...) return 1 else return bar() }
main() { foo(); }
6.6 Precision
A notable part of the algorithm above is that if we are currently analyzing a context and are
asked to analyze it again, we return K as the result of the analysis. This has similar benefits to
using K for initial dataflow values on the back edges of loops: starting with the most optimistic
assumptions about code we haven’t finished analyzing allows us to reach the best possible
fixed point. The following example program illustrates a function where the result of analysis
will be better if we assume K for recursive calls to the same context, vs. for example if we
assumed J:
int iterativeIdentity(x : int, y : int)
if x <= 0
return y
else
iterativeIdentity(x-1, y)
void main(z)
w = iterativeIdentity(z, 5)
6.7 Termination
Under what conditions will context-sensitive interprocedural analysis terminate?
Consider the algorithm above. Analyze is called only when (1) a context has not been ana-
lyzed yet, or when (2) it has just been taken off the worklist. So it is called once per reachable
context, plus once for every time a reachable context is added to the worklist.
We can bound the total number of worklist additions by (C) the number of reachable con-
texts, times (H) the height of the lattice (we don’t add to the worklist unless results for some
context changed, i.e. went up in the lattice relative to an initial assumption of K or relative
to the last analysis result), times (N) the number of callers of that reachable context. C*N is
just the number of edges (E) in the inter-context call graph, so we can see that we will do
intraprocedural analysis O(E*H) times.
Thus the algorithm will terminate as long as the lattice is of finite height and there are a
finite number of reachable contexts. Note, however, that for some lattices, notably including
constant propagation, there are an unbounded number of lattice elements and thus an un-
bounded number of contexts. If more than a finite number are not reachable, the algorithm
will not terminate. So for lattices with an unbounded number of elements, we need to adjust
the context-sensitivity approach above to limit the number of contexts that are analyzed.
34
algorithm by replacing the Context type to track only the function being called, and then having
the G ET C TX method always return the same context:
type Context
val f n : F unction
Limited contexts.. Another approach is to create contexts as in the original algorithm, but once
a certain number of contexts have been created for a given function, merge all subsequent calls
into a single context. Of course this means the algorithm cannot be sensitive to additional
contexts once the bound is reached, but if most functions have fewer contexts that are actually
used, this can be a good strategy for analyzing most of the program in a context-sensitive way
while avoiding performance problems for the minority of functions that are called from many
different contexts.
Can you implement a G ET C TX function that represents this strategy?
Call strings.. Another context sensitivity strategy is to differentiate contexts by a call string: the
call site, its call site, and so forth. In the limit, when considering call strings of arbitrary length,
this provides full context sensitivity (but is not guaranteed to terminate for arbitrary recursive
functions). Dataflow analysis results for contexts based on arbitrarylength call strings are as
precise as the results for contexts based on separate analysis for each different input dataflow
information. The latter strategy can be more efficient, however, because it reuses analysis
results when a function is called twice with different call strings but the same input dataflow
information.
In practice, both strategies (arbitrary-length call strings vs. input dataflow information)
can result in reanalyzing each function so many times that performance becomes unaccept-
able. Thus multiple contexts must be combined somehow to reduce the number of times each
function is 7 analyzed. The call-string approach provides an easy, but naive, way to do this:
call strings can be cut off at a certain length. For example, if we have call strings “a b c” and
“d e b c” (where c is the most recent call site) with a cutoff of 2, the input dataflow information
for these two call strings will be merged and the analysis will be run only once, for the context
identified by the common length-two suffix of the strings, “b c”. We can illustrate this by re-
doing the analysis of the factorial example. The algorithm is the same as above; however, we
use a different implementation of G ET C TX that computes the call string suffix:
type Context
val f n : F unction
val string : ListrInts
35
string context is a heuristic way of doing this that sometimes works well. But it can be wasteful:
if two different call strings of a given length happen to have exactly the same input analysis
information, we will do an unnecessary extra analysis, whereas it would have been better
to spend that extra analysis to differentiate calls with longer call strings that have different
analysis information.
Given a limited analysis budget, it is usually best to use heuristics that are directly based
on input information. Unfortunately these heuristics are harder to design, but they have the
potential to do much better than a call-string based approach. We will look at some examples
from the literature to illustrate this later in the course.
36
Chapter 7
Pointer Analysis
1 : z : 1
2 : p : &z
3 : p : 2
4 : print z
To analyze this program correctly we must be aware that at instruction 3, p points to z. If this
information is available we can use it in a flow function as follows:
1 : z : 1
2 : if pcondq p : &y else p : &z
3 : p : 2
4 : print z
Now constant propagation analysis must conservatively assume that z could hold either 1
or 2. We can represent this with a flow function that uses may-point-to information:
37
7.2 Andersen’s Points-To Analysis
Two common kinds of pointer analysis are alias analysis and points-to analysis. Alias analysis
computes sets S holding pairs of variables pp, q q, where p and q may (or must) point to the same
location. Points-to analysis, as described above, computes the set points-toppq, for each pointer
variable p, where the set contains a variable x if p may (or must) point to the location of the
variable x. We will focus primarily on points-to analysis, beginning with a simple but useful
approach originally proposed by Andersen (PhD thesis: “Program Analysis and Specialization
for the C Programming Language”).
Our initial setting will be C programs. We are interested in analyzing instructions that are
relevant to pointers in the program. Ignoring for the moment memory allocation and arrays,
we can decompose all pointer operations into four types: taking the address of a variable,
copying a pointer from one variable to another, assigning through a pointer, and dereferencing
a pointer:
I :: ...
| p : &x
| p : q
| p : q
| p : q
Andersen’s points-to analysis is a context-insensitive interprocedural analysis. It is also
a flow-insensitive analysis, that is an analysis that does not consider program statement order.
Context- and flow-insensitivity are used to improve the performance of the analysis, as precise
pointer analysis can be notoriously expensive in practice.
We will formulate Andersen’s analysis by generating set constraints which can later be
processed by a set constraint solver using a number of technologies. Constraint generation
for each statement works as given in the following set of rules. Because the analysis is flow-
insensitive, we do not care what order the instructions in the program come in; we simply
generate a set of constraints and solve them.
vp : &xw ãÑ lx P p address-of
copy
vp : qw ãÑ p q
vp : qw ãÑ p q
assign
vp : qw ãÑ p q dereference
The constraints generated are all set constraints. The first rule states that a constant location
lx , representation the address of x, is in the set of location pointed to by p. The second rule
states that the set of locations pointed to by p must be a superset of those pointed to by q. The
last two rules state the same, but take into account that one or the other pointer is dereferenced.
A number of specialized set constraint solvers exist and constraints in the form above can
be translated into the input for these. The dereference operation (the in p q) is not standard
in set constraints, but it can be encoded—see Fähndrich’s Ph.D. thesis for an example of how
to encode Andersen’s points-to analysis for the BANE constraint solving engine. We will treat
constraint-solving abstractly using the following constraint propagation rules:
38
p q lx Pq copy
lx P p
p q lr P p lx Pq
lx P r
assign
p q lr P q lx Pr
lx P p
dereference
We can now apply Andersen’s points-to analysis to the programs above. Note that in this
example if Andersen’s algorithm says that the set p points to only one location lz , we have
must-point-to information, whereas if the set p contains more than one location, we have only
may-point-to information.
We can also apply Andersen’s analysis to programs with dynamic memory allocation, such
as:
1: q : mallocpq
2: p : mallocpq
3: p : q
4: r : &p
5: s : mallocpq
6: r : s
7: t : &s
8: u : t
In this example, the analysis is run the same way, but we treat the memory cell allocated at
each malloc or new statement as an abstract location labeled by the location n of the allocation
point. We can use the rules:
We must be careful because a malloc statement can be executed more than once, and each
time it executes, a new memory cell is allocated. Unless we have some other means of proving
that the malloc executes only once, we must assume that if some variable p only points to one
abstract malloc’d location ln , that is still may-alias information (i.e. p points to only one of the
many actual cells allocated at the given program location) and not must-alias information.
Analyzing the efficiency of Andersen’s algorithm, we can see that all constraints can be
generated in a linear Opnq pass over the program. The solution size is Opn2 q because each of
the Opnq variables defined in the program could potentially point to Opnq other variables.
We can derive the execution time from a theorem by David McAllester published in SAS’99.
There are Opnq flow constraints generated of the form p q, p q, or p q. How many
times could a constraint propagation rule fire for each flow constraint? For a p q constraint,
the rule may fire at most Opnq times, because there are at most Opnq premises of the proper
form lx P p. However, a constraint of the form p q could cause Opn2 q rule firings, because
there are Opnq premises each of the form lx P p and lr P q. With Opnq constraints of the form
p q and Opn2 q firings for each, we have Opn3 q constraint firings overall. A similar analysis
applies for p q constraints. McAllester’s theorem states that the analysis with Opn3 q rule
firings can be implemented in Opn3 q time. Thus we have derived that Andersen’s algorithm is
cubic in the size of the program, in the worst case.
39
Interestingly, Sradharan and Fink (SAS ’09) showed that Andersen’s algorithm can be ex-
ecuted in Opn2 q time for k-sparse programs. The k-sparse assumption requires that at most k
statements dereference each variable, and that the flow graph is sparse. They also show that
typical Java programs are k-sparse and that Andersen’s algorithm scales quadratically in prac-
tice.
1 : p.f : &x
2 : p.g : &y
A field-insensitive analysis would tell us (imprecisely) that p.f could point to y. We can
modify the rules above by treating any field dereference or field assignment to p.f as a pointer
dereference p. Essentially, you can think of this as just considering all fields to be named .
vp : q.f w ãÑ p q.f
field-read
vp.f : qw ãÑ p.f q
field-assign
Now assume that objects (e.g. in Java) are represented by abstract locations l. We will have
two forms of basic facts. The first is the same as before: ln P p, where ln is an object allocated
in a new statement at line n. The second basic fact is ln P lm .f , which states that the field f of
the object represented by lm may point to an object represented by ln .
We can now process field constraints with the following rules:
p q.f lq P q lf P lq .f
lf P p
field-read
p.f q lp Pp lq Pq
lq P lp.f field-assign
If we run this analysis on the code above, we find that it can distinguish that p.f points to
x and p.g points to y.
1
note that in Java, the new expression plays the role of malloc
40
7.3 Steensgaard’s Points-To Analysis
For very large programs, a quadratic-in-practice algorithm is too inefficient. Steensgaard pro-
posed an pointer analysis algorithm that operates in near-linear time, supporting essentially
unlimited scalability in practice.
The first challenge in designing a near-linear time points-to analysis is to represent the re-
sults in linear space. This is nontrivial because over the course of program execution, any given
pointer p could potentially point to the location of any other variable or pointer q. Representing
all of these pointers explicitly will inherently take Opn2 q space.
The solution Steensgaard found is based on using constant space for each variable in the
program. His analysis associates each variable p with an abstract location named after the
variable. Then, it tracks a single points-to relation between that abstract location p and another
one q, to which it may point. Now, it is possible that in some real program p may point to
both q and some other variable r. In this situation, Steensgaard’s algorithm unifies the abstract
locations for q and r, creating a single abstract location representing both of them. Now we
can track the fact that p may point to either variable using a single points-to relationship.
For example, consider the program below:
1: p : &x
2: r : &p
3: q : &y
4: s : &q
5: r : s
Andersen’s points-to analysis would produce the following graph:
x y
p q
r s
But in Steensgaard’s setting, when we discover that r could point both to q and to p, we
must merge q and p into a single node:
x y
pq
r s
Notice that we have lost precision: by merging the nodes for p and q our graph now implies
that s could point to p, which is not the case in the actual program. But we are not done. Now
pq has two outgoing arrows, so we must merge nodes x and y. The final graph produced by
Steensgaard’s algorithm is therefore:
41
xy
pq
r s
To define Steensgaard’s analysis more precisely, we will study a simplified version of that
ignores function pointers. It can be specified as follows:
copy
vp : qw ãÑ joinpp, qq
With each abstract location p, we associate the abstract location that p points to, denoted
p. Abstract locations are implemented as a union-find2 data structure so that we can merge
two abstract locations efficiently. In the rules above, we implicitly invoke find on an abstract
location before calling join on it, or before looking up the location it points to.
The join operation essentially implements a union operation on the abstract locations.
However, since we are tracking what each abstract location points to, we must update this
information also. The algorithm to do so is as follows:
2
See any algorithms textbook
42
join(`1 , `2 )
if (find(`1 ) == find(`2 ))
return
n1 Ð `1
n2 Ð `2
union(`1 , `2 )
join(n1 , n2 )
Once again, we implicitly invoke find on an abstract location before comparing it for equal-
ity, looking up the abstract location it points to, or calling join recursively.
As an optimization, Steensgaard does not perform the join if the right hand side is not a
pointer. For example, if we have an assignment vp : q w and q has not been assigned any
pointer value so far in the analysis, we ignore the assignment. If later we find that q may hold
a pointer, we must revisit the assignment to get a sound result.
Steensgaard illustrated his algorithm using the following program:
1: a : &x
2: b : &y
3: if p then
4: y : &z
5: else
6: y : &x
7: c : &y
His analysis produces the following graph for this program:
xz
y a
c b
Rayside illustrates a situation in which Andersen must do more work than Steensgaard:
1: q : &x
2: q : &y
3: p : q
4: q : &z
After processing the first three statements, Steensgaard’s algorithm will have unified vari-
ables x and y, with p and q both pointing to the unified node. In contrast, Andersen’s algo-
rithm will have both p and q pointing to both x and y. When the fourth statement is processed,
Steensgaard’s algorithm does only a constant amount of work, merging z in with the already-
merged xy node. On the other hand, Andersen’s algorithm must not just create a points-to
relation from q to z, but must also propagate that relationship to p. It is this additional propa-
gation step that results in the significant performance difference between these algorithms.3
3
For fun, try adding a new statement r : p after statement 3. Then z has to be propagated to the points-to sets
of both p and r. In general, the number of propagations can be linear in the number of copies and the number of
address-of operators, which makes it quadratic overall even for programs in the simple form above.
43
Analyzing Steensgaard’s pointer analysis for efficiency, we observe that each of n state-
ments in the program is processed once. The processing is linear, except for find operations on
the union-find data structure (which may take amortized time Opαpnqq each) and the join oper-
ations. We note that in the join algorithm, the short-circuit test will fail at most Opnq times—at
most once for each variable in the program. Each time the short-circuit fails, two abstract loca-
tions are unified, at cost Opαpnqq. The unification assures the short-circuit will not fail again for
one of these two variables. Because we have at most Opnq operations and the amortized cost
of each operation is at most Opαpnqq, the overall running time of the algorithm is near linear:
Opn αpnqq. Space consumption is linear, as no space is used beyond that used to represent
abstract locations for all the variables in the program text.
Based on this asymptotic efficiency, Steensgaard’s algorithm was run on a 1 million line
program (Microsoft Word) in 1996; this was an order of magnitude greater scalability than
other pointer analyses known at the time.
Steensgaard’s pointer analysis is field-insensitive; making it field-sensitive would mean
that it is no longer linear.
44
Chapter 8
It has been found a serious problem to define these languages [ALGOL, FOR-
TRAN, COBOL] with sufficient rigor to ensure compatibility among all implemen-
tations...One way to achieve this would be to insist that all implementations of the
language shall satisfy the axioms and rules of inference which underlie proofs of
properties of programs expressed in the language. In effect, this is equivalent to ac-
cepting the axioms and rules of inference as the ultimately definitive specification
of the meaning of the language.
C.A.R Hoare, An Axiomatic Basis for Computer Programming,1969
tP u S tQu
P is the precondition, Q is the postcondition, and S is a piece of code of interest. Relat-
ing this back to our earlier understanding of program semantics, this can be read as “if P
holds in some state E and if xS, E y ó E 1 , then Q holds in E 1 .” We distinguish between par-
tial (tP u S tQu) and total (rP s S rQs) correctness by saying that total correctness means that,
given precondition P , S will terminate, and Q will hold; partial correctness does not make
termination guarantees. We primarily focus on partial correctness.
45
Note that we are somewhat sloppy in mixing logical variables and program variables; all
W HILE variables implicitly range over integers, and all W HILE boolean expressions are also
assertions.
We now define an assertion judgement E ( A , read “A is true in E”. The ( judgment is
defined inductively on the structure of assertions, and relies on the operational semantics of
W HILE arithmetic expressions. For example:
E ( true always
E ( e1 e2 iff xe1 , E y ó n xe2 , E y ó n
E ( e1 ¥ e2 iff xe1 , E y ó n ¥ xe2 , E y ó n
E ( A1 ^ A2 iff E ( A1 and E ( A2
...
E ( @x.A iff @n P Z.E rx : ns ( A
E ( Dx.A iff Dn P Z.E rx : ns ( A
Now we can define formally the meaning of a partial correctness assertion ( tP u S tQu:
$A $B
$A^B and
We can now write $ tP u S tQu when we can derive a triple using derivation rules. There
is one derivation rule for each statement type in the language (sound familiar?):
$ P1 ñ P $ tP u S tQu $ Q ñ Q1 consq
$ tP 1u S tQ1u
This rule is important because it lets us make progress even when the pre/post conditions
in our program don’t exactly match what we need (even if they’re logically equivalent) or are
stronger or weaker logically than ideal.
46
We can use this system to prove that triples hold. Consider ttrueu x : e tx eu, using (in
this case) the assignment rule plus the rule of consequence:
$ true ñ e e te eu x : e tx eu
$ ttrueux : etx eu
We elide a formal statement of the soundness of this system. Intuitively, it expresses
that the axiomatic proof we can derive using these rules is equivalent to the operational
semantics derivation (or that they are sound and relatively complete, that is, as complete as
the underlying logic).
47
introducing a fresh, existentially quantified variable x1 . This gives us the following strongest
postcondition for assignment:1
wppx : E, P q rE {xsP
wppS; T, Qq wppS, wppT, Qqq
wppif B then S else T, Qq B ñ wppS, Qq ^ B ñ wppT, Qq
8.2.2 Loops
As usual, things get tricky when we get to loops. Consider:
• tInv ^ B u S tInv u : Each execution of the loop preserves the invariant. This is the
inductive case of the proof.
• pInv ^ B q ñ Q : The invariant and the loop exit condition imply the postcondition.
This condition is simply demonstrating that the induction hypothesis/loop invariant we
have chosen is sufficiently strong to prove our postcondition Q.
The procedure outlined above only verifies partial correctness, because it does not reason
about how many times the loop may execute. Verifying full correctness involves placing an
upper bound on the number of remaining times the loop body will execute, typically called a
variant function, written v, because it is variant: we must prove that it decreases each time we
go through the loop. We mention this for the interested reader; we will not spend much time
on it.
1
Recall that the operation rx1 {xsE denotes the capture-avoiding substitution of x1 for x in E; we rename bound
variables as we do the substitution so as to avoid conflicts.
48
8.2.3 Proving programs
Assume a version of W HILE that annotates loops with invariants: whileinv b do S. Given such
a program, and associated pre- and post-conditions:
tP u Sinv tQu
The proof strategy constructs a verification condition V C pSannot , Qq that we seek to prove
true (usually with the help of a theorem prover). V C is guaranteed to be stronger than
wppSannot , Qq but still weaker than P : P ñ V C pSannot , Qq ñ wppSannot , Qq We compute V C
using a verification condition generation procedure V CGen, which mostly follows the defini-
tion of the wp function discussed above:
V CGenpskip, Qq Q
V CGenpS1 ; S2 , Qq V CGenpS1, V CGenpS2, Qqq
V CGenpif b then S1 else S2 , Qq b ñ V CGenpS1, Qq ^ b ñ V CGenpS2, Qq
V CGenpx : e, Qq re{xsQ
The one major point of difference is in the handling of loops:
r : 1;
i : 0;
while i m do
r : r n;
i : i 1
We wish to prove that this function computes the nth power of m and leaves the result in
r. We can state this with the postcondition r nm .
Next, we need to determine a precondition for the program. We cannot simply compute
it with wp because we do not yet know the loop invariant is—and in fact, different loop in-
variants could lead to different preconditions. However, a bit of reasoning will help. We must
have m ¥ 0 because we have no provision for dividing by n, and we avoid the problematic
computation of 00 by assuming n ¡ 0. Thus our precondition will be m ¥ 0 ^ n ¡ 0.
A good heuristic for choosing a loop invariant is often to modify the postcondition of the
loop to make it depend on the loop index instead of some other variable. Since the loop index
runs from i to m, we can guess that we should replace m with i in the postcondition r nm .
This gives us a first guess that the loop invariant should include r ni .
This loop invariant is not strong enough, however, because the loop invariant conjoined
with the loop exit condition should imply the postcondition. The loop exit condition is i ¥ m,
but we need to know that i m. We can get this if we add i ¤ m to the loop invariant. In
addition, for proving the loop body correct, we will also need to add 0 ¤ i and n ¡ 0 to the
loop invariant. Thus our full loop invariant will be r ni ^ 0 ¤ i ¤ m ^ n ¡ 0.
Our next task is to use weakest preconditions to generate proof obligations that will verify
the correctness of the specification. We will first ensure that the invariant is initially true when
the loop is reached, by propagating that invariant past the first two statements in the program:
49
tm ¥ 0 ^ n ¡ 0u
r : 1;
i : 0;
tr ni ^ 0 ¤ i ¤ m ^ n ¡ 0u
We propagate the loop invariant past i : 0 to get r n0 ^ 0 ¤ 0 ¤ m ^ n ¡ 0. We
propagate this past r : 1 to get 1 n0 ^ 0 ¤ 0 ¤ m ^ n ¡ 0. Thus our proof obligation is to
show that:
m ¥ 0 ^ n ¡ 0 ñ 1 n0 ^ 0 ¤ 0 ¤ m ^ n ¡ 0
We prove this with the following logic:
m¥0^n¡0 by assumption
1 n0 because n0 1 for all n ¡ 0 and we know n ¡ 0
0¤0 by definition of ¤
0¤m because m ¥ 0 by assumption
n¡0 by the assumption above
1 n0 ^ 0 ¤ 0 ¤ m ^ n ¡ 0 by conjunction of the above
tr ni ^ 0 ¤ i ¤ m ^ n ¡ 0 ^ i mu
r : r n;
i : i 1;
t r ni ^ 0 ¤ i ¤ m ^ n ¡ 0u
We propagate the invariant past i : i 1 to get r ni 1 ^ 0 ¤ i 1 ¤ m ^ n ¡ 0. We
propagate this past r : r n to get: r n ni 1 ^ 0 ¤ i 1 ¤ m ^ n ¡ 0. Our proof obligation
is therefore:
r ni ^ 0 ¤ i ¤ m ^ n ¡ 0 ^ i m
ñ r n ni 1 ^ 0 ¤ i 1 ¤ m ^ n ¡ 0
We can prove this as follows:
r ni ^ 0 ¤ i ¤ m ^ n ¡ 0 ^ i m by assumption
r n ni n multiplying by n
r n ni 1 definition of exponentiation
0¤i 1 because 0 ¤ i
i 1 m 1 by adding 1 to inequality
i 1¤m by definition of ¤
n¡0 by assumption
r n ni 1 ^ 0 ¤ i 1 ¤ m ^ n ¡ 0 by conjunction of the above
Last, we need to prove that the postcondition holds when we exit the loop. We have already
hinted at why this will be so when we chose the loop invariant. However, we can state the
proof obligation formally:
r ni ^ 0 ¤ i ¤ m ^ n ¡ 0 ^ i ¥ m
ñ r nm
We can prove it as follows:
50
r ni ^ 0 ¤ i ¤ m ^ n ¡ 0 ^ i ¥ m by assumption
im because i ¤ m and i ¥ m
rn m substituting m for i in assumption
51
Chapter 9
Symbolic Execution
52
is complete, the engine may go back to the branches taken and explore other paths through the
program.
To get an intuition for how symbolic analysis works, consider abstractly executing a path
through the program above. As we go along the path, we will keep track of the (potentially
symbolic) values of variables, and we will also track the conditions that must be true in order
for us to take that path. We can write this in tabular form, showing the values of the path
condition g and symbolic environment E after each line:
line g E
0 true a ÞÑ α, b ÞÑ β, c ÞÑ γ
1 true . . . , x ÞÑ 0, y ÞÑ 0, z ÞÑ 0
2 α . . . , x ÞÑ 0, y ÞÑ 0, z ÞÑ 0
5 α^β ¥5 . . . , x ÞÑ 0, y ÞÑ 0, z ÞÑ 0
9 α ^ β ¥ 5 ^ 0 0 0 3 . . . , x ÞÑ 0, y ÞÑ 0, z ÞÑ 0
In the example, we arbitrarily picked the path where the abstract value of a, i.e. α, is false,
and the abstract value of b, i.e. β, is not less than 5. We build up a path condition out of these
boolean predicates as we hit each branch in the code. The assignment to x, y, and z updates
the symbolic state E with expressions for each variable; in this case we know they are all equal
to 0. At line 9, we treat the assert statement like a branch. In this case, the branch expression
evaluates to 0 0 0 3 which is true, so the assertion is not violated.
Now, we can run symbolic execution again along another path. We can do this multiple
times, until we explore all paths in the program (exercise to the reader: how many paths are there in
the program above?) or we run out of time. If we continue doing this, eventually we will explore
the following path:
line g E
0 true a ÞÑ α, b ÞÑ β, c ÞÑ γ
1 true . . . , x ÞÑ 0, y ÞÑ 0, z ÞÑ 0
2 α . . . , x ÞÑ 0, y ÞÑ 0, z ÞÑ 0
5 α^β 5 . . . , x ÞÑ 0, y ÞÑ 0, z ÞÑ 0
6 α^β 5^γ . . . , x ÞÑ 0, y ÞÑ 1, z ÞÑ 0
6 α^β 5^γ . . . , x ÞÑ 0, y ÞÑ 1, z ÞÑ 2
9 α ^ β 5 ^ p0 1 2 3q . . . , x ÞÑ 0, y ÞÑ 1, z ÞÑ 2
Along this path, we have α ^ β 5. This means we assign y to 1 and z to 2, meaning
that the assertion 0 1 2 3 on line 9 is false. Symbolic execution has found an error in the
program!
53
guards for paths the symbolic evaluator explores. These analogs are the same as the ordinary
versions, except that in place of variables we use symbolic constants:
E P Var Ñ as
Now we can define big-step rules for the symbolic evaluation of expressions, resulting in
symbolic expressions. Since we don’t have actual values in many cases, the expressions won’t
evaluate, but variables will be replaced with symbolic constants:
xn, E y ó n big-int
We can likewise define rules for statement evaluation. These rules need to update not only
the environment E, but also a path guard g:
The rules for skip, sequence, and assignment are compositional in the expected way, with
the arithmetic expression on the right-hand side of an assignment evaluating to a symbolic
expression rather than a value. The interesting rules are the ones for if. Here, we evaluate
the condition to a symbolic predicate g 1 . In the true case, we use a SMT solver to verify that
the guard is satisfiable when conjoined with the existing path condition. If that’s the case, we
continue by evaluating the true branch symbolically. The false case is analogous.
We leave the rule for while to the reader, following the principles behind the if rules
above.
54
9.3 Heap Manipulating Programs
We can extend the idea of symbolic execution to heap-manipulating programs. Consider the
following extensions to the grammar of arithmetic expressions and statements, supporting
memory allocation with malloc as well as dereferences and stores:
a :: . . . | a | malloc
S :: . . . | a : a
Now we can define memories as a basic memory µ that can be extended based on stores
into the heap. The memory is modeled as an array, which allows SMT solvers to reason about
it using the theory of arrays:
as :: . . . | mras s
Now we can define extended version of the arithmetic expression and statement execution
semantics that take (and produce, in the case of statements) a memory:
α R E, m
xmalloc, E, my ó α big-malloc
xa, E, my ó as big-deref
xa, E, my ó mrass
xa, E, my ó as xa1, E, my ó a1s big-store
xg, E, m, a : a1y ó xg, E, mras ÞÑ a1ssy
55
Chapter 10
Program Synthesis
Note: A complete, if lengthy, resource on inductive program synthesis is the book “Program
Synthesis” by Gulwani et. al [10]. You need not read the whole thing; I encourage you to
investigate the portions of interest to you, and skim as appropriate. Many references in this
document are drawn from there; if you are interested, it contains many more.
(1) Expressing user intent. User intent (or ϕ in the above) can be expressed in a number of
ways, including logical specifications, input/output examples [6] (often with some kind of
user- or synthesizer-driven interaction), traces, natural language [5, 9, 15], or full- or partial
programs [22]. In this latter category lies reference implementations, such as executable speci-
fications (which give the desired output for a given input) or declarative specifications (which
check whether a given input/output pair is correct). Some synthesis techniques allow for
multi-modal specifications, including pre- and post- conditions, safety assertions at arbitrary
program points, or partial program templates.
Such specifications can constrain two aspects of the synthesis problem:
• Observable behavior, such as an input/output relation, a full executable specification or
safety property. This specifies what a program should compute.
• Structural properties, or internal computation steps. These are often expressed as a
sketch or template, but can be further constrained by assertions over the number or va-
riety of operations in a synthesized programs (or number of iterations, number of cache
56
misses, etc, depending on the synthesis problem in question). Indeed, one of the key
principles behind the scaling of many modern synthesis techniques lie in the way they
syntactically restrict the space of possible programs, often via a sketch, grammar, or DSL.
.
Note that basically all of the above types of specifications can be translated to constraints
in some form or another. Techniques that operate over multiple types of specifications can
overcome various challenges that come up over the course of an arbitrary synthesis problem.
Different specification types are more suitable for some types of problems than others. In ad-
dition, trace- or sketch-based specifications can allow a synthesizer to decompose a synthesis
problems into intermediate program points.
(2) Search space of possible programs. The search space naturally includes programs, often
constructed of subsets of normal programming languages. This can include a predefined set
of considered operators or control structures, defined as grammars. However, other spaces are
considered for various synthesis problems, like logics of various kinds, which can be useful
for, e.g., synthesizing graph/tree algorithms.
(3) Search technique. At a high level, there are two general approaches to logical synthesis:
• Deductive (or classic) synthesis (e.g., [17]), which maps a high-level (e.g. logical) spec-
ification to an executable implementation. Such approaches are efficient and provably
correct: thanks to the semantics-preserving rules, only correct programs are explored.
However, they require complete specifications and sufficient axiomatization of the do-
main. These approaches are classically applied to e.g., controller synthesis.
• Inductive (sometimes called syntax-guided) synthesis, which takes a partial (and often
multi-modal) specification and constructs a program that satisfies it. These techniques
are more flexible in their specification requirements and require no axioms, but often at
the cost of lower efficiency and weaker bounded guarantees on the optimality of synthe-
sized code.
Deductive synthesis shares quite a bit in common, conceptually, with compilation: rewrit-
ing a specification according to various rules to achieve a new program in at a different level of
representation. We will (very) briefly overview Denali [13], a prototypical deductive synthesis
technique, using slides. However, deductive synthesis approaches assume a complete formal
specification of the desired user intent was provided. In many cases, this can be as complicated
as writing the program itself.
This has motivated new inductive synthesis approaches, towards which considerable mod-
ern research energy has been dedicated. This category of techniques lends itself to a wide vari-
ety of search strategies, including brute-force or enumerative [1] (you might be surprised!),
probabilistic inference/belief propagation [8], or genetic programming [14]. Alternatively,
techniques based on logical reasoning delegate the search problem to a constraint solver. We
will spend more time on this set of techniques.
57
rather than by deriving the candidate directly. So, to synthesize such a program, we basi-
cally only require an interpreter, rather than a sufficient set of derivation axioms. Inductive
synthesis is applicable to a variety of problem types, such as string transformation (Flash-
Fill) [7], data extraction/processing/wrangling [6, 21], layout transformation of tables or tree-
shaped structures [23], graphics (constructing structured, repetitive drawings) [11, 4], program
repair [18, 16] (spoiler alert!), superoptimization [13], and efficient synchronization, among
others.
Inductive synthesis consists of several family of approaches; we will overview several
prominent examples, without claiming to be complete.
Syntax-Guided Synthesis (or SyGuS) formalizes the problem of program synthesis where
specification is supplemented with a syntactic template. This defines a search space of pos-
sible programs that the synthesizer effectively traverses. Many search strategies exist; two
especially well-known strategies are enumerative search (which can be remarkably effective,
though rarely scales), and deductive or top down search, which recursively reduces the problem
into simpler sub-problems.
Turn to the handout, which asks you to specify this as a synthesis problem...
Note that instead of proving that a program satisfies a given formula, we can instead dis-
prove its negation, which is:
Dl, m : pPmaxplq mq ^ pm R l _ Dx P l : m xq
If the above is satisfiable, a solver will give us a counterexample, which we can use to
strengthen the specification–so that next time the synthesis engine will give us a program that
excludes this counterexample. We can make this counterexample more useful by asking the
solver not just to provide us with an input that produces an error, but also to provide the
corresponding correct output m :
58
output. Thus we now have an additional test case for the next round of synthesis. This
counterexample-guided sythesis approach was originally introduced for SKETCH, and was
generalized to oracle-guided inductive synthesis by Jha and Seshia. Different oracles have
been developed for this type of synthesis. We will discussed component-based oracle-guided
program synthesis in detail, which illustrates the use of distinguishing oracles.
0 z0 : input0
1 z1 : input1
... ...
m zm : inputm
m 1 zm 1 : f? pz? , . . . , z? q
m 2 zm 2 : f? pz? , . . . , z? q
... ...
m n zm n : f? pz? , . . . , z? q
m n 1 return z?
The thing we have to do is fill in the ? indexes in the program above. These indexes essen-
tially define the order in which functions are invoked and what arguments they are invoked
with. We will assume that each component is used once, without loss of generality, since we
can duplicate the components.
Definitions. We will set up the problem for the solver using two sets of variables. One set
represents the input values passed to each component, and the output value that component
produces, when the program is run for a given test case. We use Ñ Ýχ i to denote the vector
of input values passed to component i and ri to denote the result value computed by that
component. So if we have a single component (numbered 1) that adds two numbers, the input
values Ñ
Ýχ 1 might be (1,3) for a given test case and the output r1 in that case would be 4. We use
Q to denote the set of all variables representing inputs and R to denote the set of all variables
representing outputs:
Q :
N Ñ Ý
i1 χ i
:
N r
R i1 i
Ñ
Ý
We also define the overall program’s inputs to be the vector Y and the program’s output
to be r.
The other set of variables determines the location of each component, as well as the loca-
tions at which each of its inputs were defined. We call these location variables. For each variable
ÝÑ
x, we define a location variable lx , which denotes where x is defined. Thus lri is the location
ÝÑ
variable for the result of component i and lχi is the vector of location variables for the inputs
of component i. So if we have lr3 5 and lχ3 is (2,4), then we will invoke component #3 at line
5, and we will pass variables z2 and z4 to it. L is the set of all location variables:
1
These notes are inspired by Section III.B of Nguyen et al., ICSE 2013 [19] ...which provides a really beautifully
clear exposition of the work that originally proposed this type of synthesis in Jha et al., ICSE 2010 [12].
59
L : tlx|x P Q Y R Y Ñ
ÝY Y ru
We will have two sets of constraints: one to ensure the program is well-formed, and the other
that ensures the program encodes the desired functionality.
Well-formedness. ψwf p denotes the well-formedness constraint. Let M |Ñ
ÝY | N , where N
is the number of available components:
ψcons pL, Rq
pl l q
def
x y
x,y PR,xy
ψacyc pL, Q, Rq
def N l l
Ñ
Ý x y
i 1x P χ i ,yri
Functionality. φf unc denotes the functionality constraint that guarantees that the solution f
satisfies the given input-output pairs:
ψconn encodes the meaning of the location variables: If two locations are equal, then the
values of the variables defined at those locations are also equal. φlib encodes the semantics of
the provided basic components, with φi representing the specification of component fi . The
rest of φf unc encodes that if the input to the synthesized function is α, the output must be β.
Almost done! φf unc provides constraints over a single input-output pair αi , βi , we still need
to generalize it over all n provided pairs t αi , βi ¡ |1 ¤ i ¤ nu:
LVal2Prog. The only real unknowns in all of θ are the values for the location variables L. So,
the solver that provides a satisfying assignment to θ is basically giving a valuation of L that
we then turn into a constructed program as follows:
Given a valuation of L, Lval2ProgpLq converts it to a program as follows: The ith line of
fj pzσ , ..., rσ q when lr i and plχ σk q, where η is the number
η
the program is zi k
1 η j j
k 1
of inputs for component fj and χkj denotes the k th input parameter of component fj . The
program output is produced in line lr .
60
Example. Assume we only have one component, +. + has two inputs: χ1 and χ2 . The
output variable is r . Further assume that the desired program f has one input Y0 (which
we call input0 in the actual program text) and one output r. Given a mapping for location
variables of: tlr ÞÑ 1, lχ1 ÞÑ 0, lχ2 ÞÑ 0, lr ÞÑ 1, lY ÞÑ 0u, then the program looks like:
0 z0 : input0
1 z1 : z0 z0
2 return z1
This occurs because the location of the variables used as input to + are both on the same
line (0), which is also the same line as the input to the program (0). lr , the return variable of the
program, is defined on line 1, which is also where the output of the + component is located.
(lr ). We added the return on line 2 as syntactic sugar.
61
Chapter 11
62
pP _ Qq ðñ P ^ Q
pP ^ Qq ðñ P _ Q
P ðñ P
pP ^ pQ _ Rqq ðñ ppP ^ Qq _ pP ^ Rqq
pP _ pQ ^ Rqq ðñ ppP _ Qq ^ pP _ Rqq
Let’s illustrate DPLL by example. Consider the following formula:
paq ^ pb _ cq ^ p a _ c _ dq ^ p c _ dq ^ p c _ d _ aq ^ pb _ dq
There is one clause with just a in it. This clause, like all other clauses, has to be true for the
whole formula to be true, so we must make a true in order for the formula to be satisfiable. We
can do this whenever we have a clause with just one literal in it, i.e. a unit clause. (Of course, if
a clause has just b, that tells us b must be false in any satisfying assignment). In this example,
we use the unit propagation rule to replace all occurrences of a with true. After simplifying, this
gives us:
pb _ cq ^ pc _ dq ^ p c _ dq ^ p c _ dq ^ pb _ dq
Now here we can see that b always occurs positively (i.e. without a in front of it within a
CNF formula). If we choose b to be true, that eliminates all occurrences of b from our formula,
thereby making it simpler—but it doesn’t change the satisfiability of the underlying formula.
An analogous approach applies when c always occurs negatively, i.e. in the form c. We say
that a literal that occurs only positively, or only negatively, in a formula is pure. Therefore, this
simplification is called the pure literal elimination rule, and applying it to the example above
gives us:
pc _ dq ^ p c _ dq ^ p c _ dq
Now for this formula, neither of the above rules applies. We just have to pick a literal and
guess its value. Let’s pick c and set it to true. Simplifying, we get:
pdq ^ p dq
After applying the unit propagation rule (setting d to true) we get:
ptrueq ^ pfalseq
which is equivalent to false, so this didn’t work out. But remember, we guessed about the
value of c. Let’s backtrack to the formula where we made that choice:
pc _ dq ^ p c _ dq ^ p c _ dq
and now we’ll try things the other way, i.e. with c false. Then we get the formula
pdq
because the last two clauses simplified to true once we know c is false. Now unit propaga-
tion sets d true and then we have shown the formula is satisfiable. A real DPLL algorithm
would keep track of all the choices in the satisfying assignment, and would report back that a
is true, b is true, c is false, and d is true in the satisfying assignment.
This procedure—applying unit propagation and pure literal elimination eagerly, then
guessing a literal and backtracking if the guess goes wrong—is the essence of DPLL. Here’s
an algorithmic statement of DPLL, adapted slightly from a version on Wikipedia:
function DPLL(φ)
63
if φ true then
return true
end if
if φ contains a false clause then
return false
end if
for all unit clauses l in φ do
φ Ð UNIT- PROPAGATE(l, φ)
end for
for all literals l occurring pure in φ do
φ Ð PURE - LITERAL - ASSIGN(l, φ)
end for
l Ð CHOOSE - LITERAL(φ)
return DPLL(φ ^ l) _ DPLL(φ ^ l)
end function
Mostly the algorithm above is straightforward, but there are a couple of notes. First of all,
the algorithm does unit propagation before pure literal assignment. Why? Well, it’s good to do
unit propagation first, because doing so can create additional opportunities to apply further
unit propagation as well as pure literal assignment. On the other hand, pure literal assignment
will never create unit literals that didn’t exist before. This is because pure literal assignment
can eliminate entire clauses but it never makes an existing clause shorter.
Secondly, the last line implements backtracking. We assume a short-cutting _ operation at
the level of the algorithm. So if the first recursive call to DPLL returns true, so does the current
call–but if it returns fall, we invoke DPLL with the chosen literal negated, which effectively
backtracks.
Exercise 1. Apply DPLL to the following formula, describing each step (unit propagation, pure
literal elimination, choosing a literal, or backtracking) and showing now it affects the formula
until you prove that the formula is satisfiable or not:
pa _ bq ^ pa _ cq ^ p a _ cq ^ pa _ cq ^ p a _ cq ^ p dq
There is a lot more to learn about DPLL, including hueristics for how to choose the literal l
to be guessed and smarter approaches to backtracking (e.g. non-chronological backtracking),
but in this class, let’s move on to consider SMT.
f pf pxq f py qq a
f p0q a 2
xy
This problem mixes linear arithmetic with the theory of uninterpreted functions (here, f
is some unknown function). The first step in the solution is to separate the two theories. We
can do this by replacing expressions with fresh variables, in a procedure named Nelson-Oppen
after its two inventors. For example, in the first formula, we’d like to factor out the subtraction,
so we generate a fresh variable and divide the formula into two:
1
This example is due to Oliveras and Rodriguez-Carbonell
64
f pe1q a // in the theory of uninterpreted functions now
e1 f pxq f py q // still a mixed formula
Now we want to separate out f pxq and f py q as variables e2 and e3, so we get:
f pe4q e5
e4 0
e5 a 2
We now have formulas in two theories. First, formulas in the theory of uninterpreted func-
tions:
f pe1q a
e2 f pxq
e3 f py q
f pe4q e5
xy
And second, formulas in the theory of arithmetic:
e1 e2 e3
e4 0
e5 a 2
xy
Notice that x y is in both sets of formulas. In SMT, we use the fact that equality is
something that every theory understands...more on this in a moment. For now, let’s run a
solver. The solver for uninterpreted functions has a congruence closure rule that states, for all
f, x, and y, if x y then f pxq f py q. Applying this rule (since x y is something we know),
we discover that f pxq f py q. Since f pxq e2 and f py q e3, by transitivity we know that
e2 e3.
But e2 and e3 are symbols that the arithmetic solver knows about, so we add e2 e3 to
the set of formulas we know about arithmetic. Now the arithmetic solver can discover that
e2 e3 0, and thus e1 e4. We communicate this discovered equality to the uninterpreted
functions theory, and then we learn that a e5 (again, using congruence closure and transi-
tivity).
This fact goes back to the arithmetic solver, which evaluates the following constraints:
e1 e2 e3
e4 0
e5 a 2
xy
e2 e3
a e5
Now there is a contradiction: a e5 but e5 a 2. That means the original formula is
unsatisfiable.
In this case, one theory was able to infer equality relationships that another theory could di-
rectly use. But sometimes a theory doesn’t figure out an equality relationship, but only certain
65
correlations - e.g. e1 is either equal to e2 or e3. In the more general case, we can simply gen-
erate a formula that represents all possible equalities between shared symbols, which would
look something like:
x¥0^y x 1 ^ py ¡2_y 1q
(note: if we had multiple theories, I am assuming we’ve already added the equality con-
straints between them, as described above)
We can then convert each arithmetic (or uninterpreted function) formula into a fresh propo-
sitional symbol, to get:
p1 ^ p2 ^ pp3 _ p4q
and then we can run a SAT solver using the DPLL algorithm. DPLL will return a satisfying
assignment, such as p1, p2, p3, p4. We then check this against each of the theories. In this case,
the theory of arithmetic finds a contradiction: p1, p2, and p4 can’t all be true, because p1 and p2
together imply that y ¥ 1. We add a clause saying that these can’t all be true and give it back
to the SAT solver:
A¤x
x¤B
where A and B are linear formulas that don’t include x. We can then eliminate x, replacing
the above formulas with the equation A ¤ B. If we have multiple formulas with x on the
left and/or right, we just conjoin the cross product. There are various optimizations that are
applied in practice, but the basic algorithm is general and provides a broad understanding of
how arithmetic solvers work.
66
Chapter 12
Concolic Testing
12.1 Motivation
Companies today spend a huge amount of time and energy testing software to determine
whether it does the right thing, and to find and then eliminate bugs. A major challenge is
writing a set of test cases that covers all of the source code, as well as finding inputs that lead
to difficult-to-trigger corner case defects.
Symbolic execution, discussed in the last lecture, is a promising approach to exploring
different execution paths through programs. However, it has significant limitations. For paths
that are long and involve many conditions, SMT solvers may not be able to find satisfying
assignments to variables that lead to a test case that follows that path. Other paths may be
short but involve computations that are outside the capabilities of the solver, such as non-
linear arithmetic or cryptographic functions. For example, consider the following function:
1 testme(int x, int y){
2 if(bbox(x)==y){
3 ERROR;
4 } else {
5 // OK
6 }
7 }
If we assume that the implementation of bbox is unavailable, or is too complicated for
a theorem prover to reason about, then symbolic execution may not be able to determine
whether the error is reachable.
Concolic testing overcomes these problems by combining concrete execution (i.e. testing)
with symbolic execution.1 Symbolic execution is used to solve for inputs that lead along a
certain path. However, when a part of the path condition is infeasible for the SMT solver to
handle, we substitute values from a test run of the program. In many cases, this allows us
to make progress towards covering parts of the code that we could not reach through either
symbolic execution or randomly generated tests.
12.2 Goals
We will consider the specific goal of automatically unit testing programs to find assertion vi-
olations and run-time errors such as divide by zero. We can reduce these problems to input
generation: given a statement s in program P , compute input i such that P piq executes s.2 For
example, if we have a statement assert x > 5, we can translate that into the code:
1
The word concolic is a portmanteau of concrete and symbolic
2
This formulation is due to Wolfram Schulte
67
1 if (!(x > 5))
2 ERROR;
Now if line 2 is reachable, the assertion is violated. We can play a similar trick with run-
time errors. For example, a statement involving division x = 3 / i can be placed under a
guard:
1 if (i != 0)
2 x = 3 / i;
3 else
4 ERROR;
12.3 Overview
Consider the testme example from the motivating section. Although symbolic analysis can-
not solve for values of x and y that allow execution to reach the error, we can generate random
test cases. These random test cases are unlikely to reach the error: for each x there is only
one y that will work, and random input generation is unlikely to find it. However, concolic
testing can use the concrete value of x and the result of running bbox(x) in order to solve for
a matching y value. Running the code with the original x and the solution for y results in a
test case that reaches the error.
In order to understand how concolic testing works in detail, consider a more realistic and
more complete example:
1 int double (int v) {
2 return 2*v;
3 }
4
5 void bar(int x, int y) {
6 z = double (y);
7 if (z == x) {
8 if (x > y+10) {
9 ERROR;
10 }
11 }
12 }
We want to test the function bar. We start with random inputs such as x 22, y 7. We
then run the test case and look at the path that is taken by execution: in this case, we compute
z 14 and skip the outer conditional. We then execute symbolically along this path. Given
inputs x x0 , y y0 , we discover that at the end of execution z 2 y0 , and we come up with
a path condition 2 y0 x0 .
In order to reach other statements in the program, the concolic execution engine picks a
branch to reverse. In this case there is only one branch touched by the current execution path;
this is the branch that produced the path condition above. We negate the path condition to get
2 y0 x0 and ask the SMT solver to give us a satisfying solution.
Assume the SMT solver produces the solution x0 2, y0 1. We run the code with that
input. This time the first branch is taken but the second one is not. Symbolic execution returns
the same end result, but this time produces a path condition 2 y0 x0 ^ x0 ¤ y0 10.
Now to explore a different path we could reverse either test, but we’ve already explored the
path that involves negating the first condition. So in order to explore new code, the concolic
execution engine negates the condition from the second if statement, leaving the first as-is.
68
We hand the formula 2 y0 x0 ^ x0 ¡ y0 10 to an SMT solver, which produces a solution
x0 30, y0 15. This input leads to the error.
The example above involves no problematic SMT formulas, so regular symbolic execution
would suffice. The following example illustrates a variant of the example in which concolic
execution is essential:
1 int foo(int v) {
2 return v*v%50;
3 }
4
5 void baz(int x, int y) {
6 z = foo(y);
7 if (z == x) {
8 if (x > y+10) {
9 ERROR;
10 }
11 }
12 }
Although the code to be tested in baz is almost the same as bar above, the problem is more
difficult because of the non-linear arithmetic and the modulus operator in foo. If we take the
same two initial inputs, x 22, y 7, symbolic execution gives us the formula z py0 y0 q%50,
and the path condition is x0 py0 y0 q%50. This formula is not linear in the input y0 , and so it
may defeat the SMT solver.
We can address the issue by treating foo, the function that includes nonlinear computation,
concretely instead of symbolically. In the symbolic state we now get z f oopy0 q, and for y0 7
we have z 49. The path condition becaomse f oopy0 q x0 , and when we negate this we get
f oopy0 q x0 , or 49 x0 . This is trivially solvable with x0 49. We leave y0 7 as before;
this is the best choice because y0 is an input to f oopy0 q so if we change it, then setting x0 49
may not lead to taking the first conditional. In this case, the new test case of x 49, y 7 finds
the error.
12.4 Implementation
Ball and Daniel [2] give the following pseudocode for concolic execution (which they call dy-
namic symbolic execution):
1 i = an input to program P
2 while defined(i):
3 p = path covered by execution P(i)
4 cond = pathCondition(p)
5 s = SMT(Not(cond))
6 i = s.model()
Broadly, this just systematizes the approach illustrated in the previous section. However, a
number of details are worth noting:
First, when negating the path condition, there is a choice about how to do it. As discussed
above, the usual approach is to put the path conditions in the order in which they were gen-
erated by symbolic execution. The concolic execution engine may target a particular region of
code for execution. It finds the first branch for which the path to that region diverges from the
current test case. The path conditions are left unchanged up to this branch, but the condition
for this branch is negated. Any conditions beyond the branch under consideration are simply
omitted. With this approach, the solution provided by the SMT solver will result in execution
69
reaching the branch and then taking it in the opposite direction, leading execution closer to the
targeted region of code.
Second, when generating the path condition, the concolic execution engine may choose
to replace some expressions with constants taken from the run of the test case, rather than
treating those expressions symbolically. These expressions can be chosen for one of several
reasons. First, we may choose formulas that are difficult to invert, such as non-linear arithmetic
or cryptographic hash functions. Second, we may choose code that is highly complex, leading
to formulas that are too large to solve efficiently. Third, we may decide that some code is not
important to test, such as low-level libraries that the code we are writing depends on. While
sometimes these libraries could be analyzable, when they add no value to the testing process,
they simply make the formulas harder to solve than they are when the libraries are analyzed
using concrete data.
12.5 Acknowledgments
The structure of these notes and the examples are adapted from a presentation by Koushik
Sen.
70
Bibliography
[2] T. Ball and J. Daniel. Deconstructing dynamic symbolic execution. In Proceedings of the
2014 Marktoberdorf Summer School on Dependable Software Systems Engineering, 2015.
[3] W. R. Bush, J. D. Pincus, , and D. J. Sielaff. A static analyzer for finding dynamic program-
ming errors. Software—Practice and Experience, 30:775–802, 2000.
[4] R. Chugh, B. Hempel, M. Spradlin, and J. Albers. Programmatic and direct manipulation,
together at last. SIGPLAN Not., 51(6):341–354, June 2016.
[5] A. Desai, S. Gulwani, V. Hingorani, N. Jain, A. Karkare, M. Marron, S. R, and S. Roy. Pro-
gram synthesis using natural language. In Proceedings of the 38th International Conference
on Software Engineering, ICSE ’16, pages 345–356, New York, NY, USA, 2016. ACM.
[7] S. Gulwani, W. R. Harris, and R. Singh. Spreadsheet data manipulation using examples.
Commun. ACM, 55(8):97–105, Aug. 2012.
[9] S. Gulwani and M. Marron. Nlyze: Interactive programming by natural language for
spreadsheet data analysis and manipulation. In Proceedings of the 2014 ACM SIGMOD
International Conference on Management of Data, SIGMOD ’14, pages 803–814, New York,
NY, USA, 2014. ACM.
[10] S. Gulwani, O. Polozov, and R. Singh. Program synthesis. Foundations and Trends in Pro-
gramming Languages, 4(1-2):1–119, 2017.
[11] B. Hempel and R. Chugh. Semi-automated svg programming via direct manipulation.
In Proceedings of the 29th Annual Symposium on User Interface Software and Technology, UIST
’16, pages 379–390, New York, NY, USA, 2016. ACM.
71
[13] R. Joshi, G. Nelson, and K. Randall. Denali: A goal-directed superoptimizer. SIGPLAN
Not., 37(5):304–314, May 2002.
[14] G. Katz and D. Peled. Genetic programming and model checking: Synthesizing new
mutual exclusion algorithms. In Proceedings of the 6th International Symposium on Automated
Technology for Verification and Analysis, ATVA ’08, pages 33–47, Berlin, Heidelberg, 2008.
Springer-Verlag.
[15] V. Le, S. Gulwani, and Z. Su. Smartsynth: Synthesizing smartphone automation scripts
from natural language. In Proceeding of the 11th Annual International Conference on Mobile
Systems, Applications, and Services, MobiSys ’13, pages 193–206, New York, NY, USA, 2013.
ACM.
[16] C. Le Goues, T. Nguyen, S. Forrest, and W. Weimer. GenProg: A generic method for
automated software repair. IEEE Transactions on Software Engineering, 38(1):54–72, 2012.
[17] Z. Manna and R. J. Waldinger. Toward automatic program synthesis. Commun. ACM,
14(3):151–165, Mar. 1971.
[18] S. Mechtaev, J. Yi, and A. Roychoudhury. Angelix: Scalable Multiline Program Patch
Synthesis via Symbolic Analysis. In International Conference on Software Engineering, ICSE
’16, pages 691–701, 2016.
[19] H. D. T. Nguyen, D. Qi, A. Roychoudhury, and S. Chandra. Semfix: Program repair via
semantic analysis. In Proceedings of the 2013 International Conference on Software Engineering,
ICSE ’13, pages 772–781, Piscataway, NJ, USA, 2013. IEEE Press.
[20] O. Polozov and S. Gulwani. Flashmeta: A framework for inductive program synthesis.
SIGPLAN Not., 50(10):107–126, Oct. 2015.
[21] R. Singh and S. Gulwani. Transforming spreadsheet data types using examples. In Pro-
ceedings of the 43rd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Program-
ming Languages, POPL ’16, pages 343–356, New York, NY, USA, 2016. ACM.
[22] A. Solar-Lezama. Program Synthesis by Sketching. PhD thesis, Berkeley, CA, USA, 2008.
AAI3353225.
72