SlideShare a Scribd company logo
A Verifiable SSA Program Representation for Aggressive
Compiler Optimization
Vijay S. Menon1 Neal Glew1 Brian R. Murphy2 Andrew McCreight3 ∗ Tatiana Shpeisman1
Ali-Reza Adl-Tabatabai1 Leaf Petersen1
1
Intel Labs 2
Intel China Research Center 3
Dept. of Computer Science, Yale University
Santa Clara, CA 95054 Beijing, China New Haven, CT 06520
{vijay.s.menon, brian.r.murphy, tatiana.shpeisman, ali-reza.adl-tabatabai, leaf.petersen}@intel.com aglew@acm.org
andrew.mccreight@yale.edu
Abstract
We present a verifiable low-level program representation to em-
bed, propagate, and preserve safety information in high perfor-
mance compilers for safe languages such as Java and C#. Our rep-
resentation precisely encodes safety information via static single-
assignment (SSA) [11, 3] proof variables that are first-class con-
structs in the program.
We argue that our representation allows a compiler to both (1)
express aggressively optimized machine-independent code and
(2) leverage existing compiler infrastructure to preserve safety
information during optimization. We demonstrate that this ap-
proach supports standard compiler optimizations, requires minimal
changes to the implementation of those optimizations, and does not
artificially impede those optimizations to preserve safety.
We also describe a simple type system that formalizes type
safety in an SSA-style control-flow graph program representation.
Through the types of proof variables, our system enables composi-
tional verification of memory safety in optimized code.
Finally, we discuss experiences integrating this representation
into the machine-independent global optimizer of STARJIT, a
high-performance just-in-time compiler that performs aggressive
control-flow, data-flow, and algebraic optimizations and is compet-
itive with top production systems.
Categories and Subject Descriptors D.3.1 [Programming Lan-
guages]: Formal Definitions and Theory; D.3.4 [Programming
Languages]: Compilers; D.3.4 [Programming Languages]: Opti-
mization; F.3.1 [Logics and Meanings of Programs]: Specifying
and Verifying and Reasoning about Programs
General Terms Performance, Design, Languages, Reliability,
Theory, Verification
Keywords Typed Intermediate Languages, Proof Variables, Safety
Dependences, Check Elimination, SSA Formalization, Type Sys-
tems, Typeability Preservation, Intermediate Representations
∗ Supported in part by NSF grants CCR-0208618 and CCR-0524545.
[copyright notice will appear here]
1. Introduction
In the past decade, safe languages have become prevalent in the
general software community and have gained wide acceptance
among software developers. Safe languages such as Java and C# are
particularly prominent. These languages provide a C++-like syn-
tax and feature set in conjunction with verifiable safety properties.
Foremost among these properties is memory safety, the guarantee
that a program will only read or write valid memory locations.
Memory safety is crucial to both robustness and security. It pre-
vents common programmer memory errors and security exploits
such as buffer overruns through a combination of compile-time
and run-time checks.
Both Java and C# were designed to allow programs to be com-
piled and distributed via bytecode formats. These formats retain the
crucial safety properties of the source language and are themselves
statically verifiable. Managed runtime environments (MRTEs),
such as the Java Virtual Machine (JVM) or the Common Lan-
guage Infrastructure (CLI), use static verification to ensure that no
memory errors have been introduced inadvertently or maliciously
before executing bytecode programs.
Bytecodes, however, are still rather high-level compared to na-
tive machine code. Runtime checks (e.g., array bounds checks)
are built into otherwise potentially unsafe operations (e.g., mem-
ory loads) to ease the verification process. To obtain acceptable
performance, MRTEs compile programs using a just-in-time (JIT)
compiler. A JIT compiler performs several control- and data-flow
compiler transformations and produces optimized native machine
code. In the process, runtime checks are often eliminated or sepa-
rated from the potentially unsafe operations that they protect. As far
as we are aware, all production Java and CLI JIT compilers remove
safety information during the optimization process: optimized low
level code or generated machine code is not easily verifiable. From
a security perspective, this precludes the use of optimized low level
code as a persistent and distributable format. Moreover, from a reli-
ability perspective it requires that the user trust that complex com-
piler transformations do not introduce memory errors.
In recent years, researchers have developed proof languages
(e.g., PCC [19] and TAL [18]) that allow a compiler to embed
safety proofs into low-level code, along with verification tech-
niques to validate those proofs. They have demonstrated certifying
compilers that can compile Java and safe C-like languages [20, 8,
17, 13] while both performing optimizations and generating safety
proofs. Nevertheless, although the proof language and verification
process is well-developed, implementing or modifying existing op-
timizations to correctly generate and/or preserve safety information
is still an arduous and poorly understood process.
POPL ’06 Submission 1 2005/11/15
In this paper, we introduce a new program representation frame-
work for safe, imperative, object-oriented languages to aid in the
generation, propagation, and verification of safety information
through aggressive compiler optimization. In this representation
we encode safety dependences, the dependences between poten-
tially unsafe operations and the control points that guarantee their
safety, as abstract proof variables. These proof variables are purely
static: they have no runtime semantics. Nevertheless, they are first
class constructs produced by control points and consumed by po-
tentially unsafe instructions. From the perspective of most compiler
transformations, they are the same as any other variable.
We argue that this representation is particularly well-suited to
use as an intermediate representation for an aggressively optimiz-
ing compiler. We demonstrate that it supports common advanced
compiler optimizations without artificially constraining or exten-
sively modifying them. In particular, we demonstrate that by carry-
ing proof values in normal variables a compiler can leverage exist-
ing transformations such as SSA construction, copy propagation,
and dead code elimination to place, update and eliminate proof
variables.
We illustrate our ideas in the context of the machine-independent
global optimizer of STARJIT [1], a dynamic optimizing compiler
for Java and C#. STARJIT was designed as a high-performance op-
timizing compiler and is competitive in performance with the best
production MRTE systems. We describe a prototype integration of
our ideas into STARJIT’s internal representation, and we discuss
how it is able to preserve safety information through a varied set
of aggressive optimizations. The original motivation for the safety
dependence representation described in this paper was for opti-
mization rather than safety. However, a prototype implementation
of a verifier has also been developed, and this paper is intended
to provide both a description of the safety dependence mechanism
and a theoretical development of a type system based upon it.
In particular, our paper makes the following contributions:
1. We introduce a safe low-level imperative program representa-
tion that combines static single-assignment (SSA) form with
explicit safety dependences, and we illustrate how it can be used
to represent highly optimized code.
2. We present a simple type system to verify memory safety of
programs in this representation. To the best of our knowledge,
this type system is the first to formalize type checking in an
SSA representation. While SSA is in some sense equivalent to
CPS, the details are sufficiently different that our type system is
quite unlike the usual lambda-calculus style type systems and
required new proof techniques.
3. We demonstrate the utility of this program representation in a
high-performance compiler, and we describe how a compiler
can leverage its existing framework to preserve safety informa-
tion. In particular, we demonstrate that only optimizations that
directly affect memory safety, such as bounds check elimination
and strength reduction of address calculations, require signifi-
cant modification.
The remainder of the paper is organized as follows. In Section 2,
we motivate the explicit representation of safety dependence in an
optimizing compiler and describe how to do this via proof variables
in a low-level imperative program representation. In Section 3, we
describe a formal core language specifically dealing with array-
bounds checks and present a type system with which we can verify
programs in SSA form. In Section 4, we demonstrate how a com-
piler would lower a Java program to the core language and illustrate
how aggressive compiler optimizations produce efficient and veri-
fiable code. In Section 5, we informally describe extensions to our
core language to capture complete Java functionality. In Section 6,
if (a!=null)
while (!done) {
b = (B)a;
· · · = · · · b.x · · ·
· · ·
}
Figure 1. Field load in loop
we discuss the status of our current implementation, and, finally, in
Sections 7 and 8 we discuss related work and conclude.
2. Motivation
We define a potentially unsafe instruction as any instruction that,
taken out of context, might fault or otherwise cause an illegal
memory access at runtime. Some instructions, taken independently,
are inherently unsafe. A load instruction may immediately fault if
it accesses protected memory or may trigger an eventual crash by
reading an incorrectly typed value. A store may corrupt memory
with an illegal value (e.g., if an arbitrary integer replaces an object’s
virtual table).
Consider, for example, the field access in Figure 1. Assuming
C++-like semantics, the operation b.x dereferences memory with
no guarantee of safety. In general, C++ does not guarantee that b
refers to a real object of type B: b may hold an an integer that
faults when used as a pointer.
Assuming Java semantics, however, the field access itself
checks at runtime that b does not point to a null location. If the
check succeeds, the field access executes the load; otherwise, it
throws an exception, bypassing the load. By itself, this built-in
check does not ensure safety: the load also depends on the preced-
ing cast, which dynamically checks that the runtime type of b is
in fact compatible with the type B. If the check succeeds, the cast
executes the load; otherwise, it throws an exception, bypassing the
load.
Typically, the safety of a potentially unsafe instruction depends
on a set of control flow points. We refer to this form of dependence
as safety dependence. In this example, the safety of the load de-
pends on the cast that establishes its type. We call an instruction
contextually safe when its corresponding safety dependences guar-
antee its safety. To verify the output of a compiler optimization, we
must prove that each instruction is contextually safe.
2.1 Safety In Java
In Java and the verifiable subset of CLI, a combination of static ver-
ification and runtime checks guarantee the contextual safety of indi-
vidual bytecode instructions. Static type checking establishes that
variables have the appropriate primitive or object type. Runtime
checks such as type tests (for narrowing operations), null pointer
tests, and array bounds tests detect conditions that would cause a
fault or illegal access and throw a language-level runtime excep-
tion instead.
Figure 2 shows Java-like bytecode instructions (using pseudo-
registers in place of stack locations for clarity) for the code of
Figure 1. The Java type system guarantees that variable b has type
B at compile time, while the getfield instruction guarantees non-
null access by testing for null at runtime. The check and the static
verifier together guarantee that the load operation will not trigger
an illegal memory access.
2.2 Safety in a Low-Level Representation
The Java bytecode format was not intended to be an intermedi-
ate program representation for an optimizing compiler. There are
a number of reasons why such a format is not suitable, but here we
POPL ’06 Submission 2 2005/11/15
ifnull a goto EXIT
L :
ifeq done goto EXIT
b := checkcast(a, B)
t1 := getfield(b, B::x)
· · ·
goto L
EXIT :
Figure 2. Field load with Java-like bytecode
if a = null goto EXIT
L :
if done = 0 goto EXIT
checkcast(a, B)
checknull(a)
t2 := getfieldaddr(a, B::x)
t1 := ld(t2)
· · ·
goto L
EXIT :
Figure 3. Field load lowered in erasure-style representation
will focus only on those related to safety. First, bytecodes hide re-
dundant check elimination opportunities. For example, in Figure 2,
optimizations can eliminate the null check built into the getfield
instruction because of the ifnull instruction. Even though sev-
eral operations have built-in exception checks, programmers usu-
ally write their code to ensure that these checks never fail, so such
optimization opportunities are common in Java programs.
Second, extraneous aliasing introduced to encode safety prop-
erties hides optimization opportunities. In Figures 1 and 2, vari-
able b represents a copy of a that has the type B. Any use of a
that requires this type information must use b instead. While this
helps static verification, it hinders optimization. The field access
must establish that b is not null, even though the ifnull statement
establishes that property on a. To eliminate the extra check, a re-
dundancy elimination optimization must reason about aliasing due
to cast operations; this is beyond the capabilities of standard algo-
rithms [16, 5].
In the absence of a mechanism for tracking safety dependences,
STARJIT would lower a code fragment like this to one like that
in Figure 3. Note that the ld operation is potentially unsafe and is
safety dependent on the null check. In this case, however, the safety
dependence between the null check and the load is not explicit. Al-
though the instructions are still (nearly) adjacent in this code, there
is no guarantee that future optimizations will leave them so. Fig-
ure 4 roughly illustrates the code that STARJIT would produce for
our example. Redundant checks are removed by a combination of
partial loop peeling (to expose redundant control flow) and com-
mon subexpression elimination. The invariant address field calcu-
lation is hoisted via code motion. In this case, the dependence of the
load on the operations that guarantee its safety (specifically, the if
and checkcast statements) has become obscured. We refer to this
as an erasure-style low-level representation, as safety information
is effectively erased from the program.
An alternative representation embeds safety information di-
rectly into the values and their corresponding types. The Java lan-
guage already does this for type refinement via cast operations.
This approach also applies to null checks, as shown in Figure 5. The
SafeTSA representation takes this approach, extending it to array
bounds checks [24, 2] as well. We refer to this as a refinement-style
representation. In this representation, value dependences preserve
the safety dependence between a check and a load. To preserve
t2 := getfieldaddr(a, B::x)
if a = null goto EXIT
if done = 0 goto EXIT
checkcast(a, B)
L :
t1 := ld(t2)
· · ·
if done 6= 0 goto L
EXIT :
Figure 4. Field load optimized in erasure-style representation
if a = null goto EXIT
L :
if done = 0 goto EXIT
b := checkcast(a, B)
t3 := checknull(b)
t2 := getfieldaddr(t3, B::x)
t1 := ld(t2)
· · ·
goto L
EXIT :
Figure 5. Field load lowered in refinement-style representation
safety, optimizations must preserve the value flow between the
check and the load. Check elimination operations (such as the
checknull in Figure 5) may be eliminated by optimization, but
the values they produce (e.g., t2) must be redefined in the process.
From an optimization standpoint, a refinement-style represen-
tation is not ideal. The safety dependence between the check and
the load is not direct. Instead, it is threaded through the address
field calculation, which is really just an addition operation. While
the load itself cannot be performed until the null test, the address
calculation is always safe. A code motion or instruction scheduling
compiler optimization should be free to move it above the check if
it is deemed beneficial. In Figure 3, it is clearly legal. In Figure 5,
it is no longer possible. The refinement-style representation adds
artificial constraints to the program to allow safety to be checked.
In this case, the address calculation is artificially dependent on the
check operation.
A refinement-style representation also obscures optimization
opportunities by introducing multiple names for the same value.
Optimizations that depend on syntactic equivalence of expressions
(such as the typical implementation of redundancy elimination) be-
come less effective. In Figure 3, a is syntactically compared to
null twice. In Figure 5, this is no longer true. In general, syntac-
tically equivalent operations in an erasure-style representation may
no longer be syntactically equivalent in a refinement-style repre-
sentation.
2.3 A Proof Passing Representation
Neither the erasure-style nor refinement-style representations pre-
cisely represent safety dependences. The erasure-style representa-
tion omits them altogether, while the refinement-style representa-
tion encodes them indirectly. As a result, the erasure-style rep-
resentation is easy to optimize but difficult to verify, while the
refinement-style is difficult to optimize but easy to verify.
To bridge this gap, we propose the use of a proof passing
representation that encodes safety dependence directly into the
program representation through proof variables. Proof variables act
as capabilities for unsafe operations (similar to the capabilities of
Walker et al. [25]). The availability of a proof variable represents
the availability of a proof that a safety property holds. A potentially
unsafe instruction must use an available proof variable to ensure
POPL ’06 Submission 3 2005/11/15
[s1, s2] if a = null goto EXIT
L :
if done = 0 goto EXIT
s3 := checkcast(a, B)
s4 := checknull(a)
t2 := getfieldaddr(a, B::x)
s5 := pfand(s3, s4)
t1 := ld(t2) [s5]
· · ·
goto L
EXIT :
Figure 6. Field load lowered in a proof passing representation
t2 := getfieldaddr(a, B::x)
[s1, s2] if a = null goto EXIT
L :
if done = 0 goto EXIT
s3 := checkcast(a, B)
s4 := s1
s5 := pfand(s3, s4)
t1 := ld(t2) [s5]
· · ·
goto L
EXIT :
Figure 7. Field load with CSE and Code Motion
contextual safety. This methodology relates closely to mechanisms
proposed for certified code by Crary and Vanderwaart [10] and
Shao et al. [22] in the context of the lambda calculus. We discuss
the relationship of our approach to this work in Section 7.
Proof variables do not consume any physical resources at run-
time: they represent abstract values and only encode safety de-
pendences. Nevertheless, they are first-class constructs in our rep-
resentation. They are generated by interesting control points and
other relevant program points, and consumed by potentially unsafe
instructions as operands guaranteeing safety. Most optimizations
treat proof variables like other program variables.
Figure 6 demonstrates how we represent a load operation in a
proof passing representation. As in Figure 5, we represent safety
through value dependences, but instead of interfering with existing
values, we insert new proof variables that directly model the safety
dependence between the load and both check operations.
Figures 7 to 10 represent the relevant transformations performed
by STARJIT to optimize this code. In Figure 7, we illustrate two op-
timizations. First, STARJIT’s common subexpression elimination
pass eliminates the redundant checknull operation. When STAR-
JIT detects a redundant expression in the right hand side of an in-
struction, it replaces that expression with the previously defined
variable. The if statement defines the proof variable s1 if the test
fails. This variable proves the proposition a 6= null. At the defi-
nition of s4, the compiler detects that a 6= null is available, and
redefines s4 to be a copy of s1. STARJIT updates a redundant proof
variable the same way as any other redundant variable.
Second, STARJIT hoists the definition of t2, a loop invariant
address calculation, above the loop. Even though the computed ad-
dress may be invalid at this point, the address calculation is always
safe; we require a proof of safety only on a memory operation that
dereferences the address.
Figure 8 shows a step of copy propagation, which propagates s1
into the load instruction and eliminates the use of s4, allowing dead
code elimination to remove the definition of s4.
Figure 9 illustrates the use of partial loop peeling to expose re-
dundant control flow operations within the loop. This transforma-
t2 := getfieldaddr(a, B::x)
[s1, s2] if a = null goto EXIT
L :
if done = 0 goto EXIT
s3 := checkcast(a, B)
s5 := pfand(s3, s1)
t1 := ld(t2) [s5]
· · ·
goto L
EXIT :
Figure 8. Field load with Copy Propagation
t2 := getfieldaddr(a, B::x)
[s1, s2] if a = null goto EXIT
if done = 0 goto EXIT
s1
3 := checkcast(a, B)
L :
s2
3 := φ(s1
3, s3
3)
s5 := pfand(s2
3, s1)
t1 := ld(t2) [s5]
· · ·
if done = 0 goto EXIT
s3
3 := checkcast(a, B)
goto L
EXIT :
Figure 9. Field load with Partial Loop Peeling
t2 := getfieldaddr(a, B::x)
[s1, s2] if a = null goto EXIT
if done = 0 goto EXIT
s3 := checkcast(a, B)
s5 := pfand(s3, s1)
L :
t1 := ld(t2) [s5]
· · ·
if done 6= 0 goto L
EXIT :
Figure 10. Field load with 2nd CSE and Branch Reversal
tion duplicates the test on done and the checkcast operation, and
makes the load instruction the new loop header. The proof variable
s3 is now defined twice, where each definition establishes that a
has type B on its corresponding path. The compiler leverages SSA
form to establish that the proof variable is available within the loop.
Finally, in Figure 10, another pass of common subexpression
elimination eliminates the redundant checkcast. Copy propaga-
tion propagates the correct proof variable, this time through a re-
dundant phi instruction. Note, that this final code is equivalent to
the erasure-style representation in Figure 4 except that proof vari-
ables provide a direct representation of safety. In Figure 10, it is
readily apparent that the if and checkcast statements establish
the safety of the load instruction.
In the next section we formalize our approach as a small core
language, and the following sections show its use and preservation
across compiler optimizations and extension to full Java.
3. Core Language
In this section we describe a small language that captures the main
ideas of explicit safety dependences through proof variables. As
usual with core languages, we wish to capture just the essence of
the problem and no more. The issue at hand is safety dependences,
and to keep things simple we will consider just one such depen-
POPL ’06 Submission 4 2005/11/15
(P, L1, n1, b.i) 7→ (P, L2, n2, pc) where:
P(b.i) L2 n2 pc Side conditions
p L1{x1 := L1(x2)} n1 b.(i + 1) p[n1] = x1 := x2
x : τ := i L1{x := i} n1 b.(i + 1)
x1 : τ := x2 L1{x1 := L1(x2)} n1 b.(i + 1)
x1 : τ := newarray(x2, x3) L1{x1 := v1} n1 b.(i + 1) L1(x2) = n, L1(x3) = v3, v1 = hv3, . . . , v3
| {z }
n
i
x1 : τ := newarray(x2, x3) L1{x1 := v1} n1 b.(i + 1) L1(x2) = i, i < 0, v1 = hi
x1 : τ := len(x2) L1{x1 := n} n1 b.(i + 1) L1(x2) = hv0, . . . , vn−1i
x1 : τ := base(x2) L1{x2 := v@0} n1 b.(i + 1) L1(x2) = v, v = hv′i
x1 : τ := x2 bop x3 L1{x1 := i4} n1 b.(i + 1) L1(x2) = i2, L1(x3) = i3, i4 = i2 bop i3
x1 : τ := x2 bop x3 L1{x1 := v@i4} n1 b.(i + 1) L1(x2) = v@i2, L1(x3) = i3, i4 = i2 bop i3
x1 : τ := ld(x2) [x3] L1{x1 := vi} n1 b.(i + 1) L1(x2) = hv0, . . . , vni@i, 0 ≤ i ≤ n
x1 : τ := pffact(x2) L1{x1 := true} n1 b.(i + 1)
x : τ := pfand(y) L1{x := true} n1 b.(i + 1)
[x1 : τ1, x2 : τ2] if x3 rop x4 goto b′ L1{x1 := true} edgeP (b, b + 1) (b + 1).0 L1(x3) = i3, L1(x4) = i4, ¬(i3 rop i4)
[x1 : τ1, x2 : τ2] if x3 rop x4 goto b′ L1{x2 := true} edgeP (b, b′) b′.0 L1(x3) = i3, L1(x4) = i4, i3 rop i4
goto b′ L1 edgeP (b, b′) b′.0
Figure 11. Operational semantics
dence, namely, bounds checking for arrays. In particular, we con-
sider a compiler with separate address arithmetic, load, and store
operations, where the type system must ensure that a load or store
operation is applied only to valid pointers. Moreover, since the ba-
sic safety criteron for a store is the same as for a load, namely, that
the pointer is valid, we consider only loads; adding stores to our
core language adds no interesting complications. Not considering
stores further allows us to avoid modelling the heap explicitly, but
to instead use a substitution semantics which greatly simplifies the
presentation.
The syntax of our core language is given as follows:
Prog. States S ::= (P, L, n, pc)
Programs P ::= B
Blocks B ::= p; ι; c
Phi Instructions p ::= x : τ := φ(x)
Instructions ι ::= x : τ := r
Right-hand sides r ::= i | x | newarray(x1, x2) |
len(x) | base(x) |
x1 bop x2 | ld(x1) [x2] |
pffact(x) | pfand(x)
Binary Ops bop ::= + | −
Transfers c ::= goto n | halt |
[x1 : τ1, x2 : τ2] if x3 rop x4
goto n
Relations rop ::= <|≤|=|6=
Environments L ::= x := v
Values v ::= i | hvi | hvi@i | true
Prog. Counters pc ::= n1.n2
Here i ranges over integer constants, x ranges over variables, n
ranges over natural numbers, and φ is the phi-operation of SSA.
We use the bar notation introduced in Featherweight Java [15]: B
abbreviates B0, . . . , Bn, x := v abbreviates x0 := v0, . . . , xn :=
vn, et cetera. We also use the bar notation in type rules to ab-
breviate a sequence of typing judgements in the obvious way.
In addition to the grammar above, programs are subject to a
number of context-sensitive restrictions. In particular, the n in
[x1 : τ1, x2 : τ2] if x3 rop x4 goto n and goto n must be a
block number in the program (i.e., if the program is B0, . . . , Bm
then 0 ≤ n ≤ m); the transfer in the last block must be a goto
or halt; the number of variables in a phi instruction must equal the
number of incoming edges (as defined below) to the block in which
it appears; the variables assigned in the phi instructions of a block
must be distinct.
Informally, the key features of our language are the following.
The operation base(x) takes an array and creates a pointer to the
element at index zero. The arithmetic operations can be applied to
such pointers and an integer to compute a pointer to a different in-
dex. The ld(x1) [x2] operation loads the value pointed to by the
pointer in x1. The variable x2 is a proof variable and conceptually
contains a proof that x1 is a valid pointer: that is, that it points to an
in-bounds index. The typing rules ensure that x1 is valid by requir-
ing x2 to contain an appropriate proof. The operations pffact(x)
and pfand(x) construct proofs. For pffact(x) a proof of a for-
mula based on the definition of x is constructed. For example, if
x’s definition is x : int := len(y) then pffact(x) constructs a
proof of x = len(y). A complete definition of the defining facts
of instructions appears in Figure 14. For pfand(x1, . . . , xn), x1
through xn are also proof variables, and a proof of the conjunction
is returned. Values of the form hv0, . . . , vni@i represent pointers
to array elements: in this case a pointer to the element at index i
of an array of type hv0, . . . , vni. Such a pointer is valid if i is in
bounds (that is, if 0 ≤ i ≤ n) and invalid otherwise. The typing
rules must ensure that only valid pointers are loaded from, with
proof variables used to provide evidence of validity. The final un-
usual aspect of the language is that branches assign proofs to proof
variables that reflect the condition being branched on. For exam-
ple, in the branch [x1 : τ1, x2 : τ2] if x3=x4 goto n, a proof of
x3 6= x4 is assigned to x1 along the fall-through edge, and a proof
of x3 = x4 is assigned to x2 along the taken edge. These proofs
can then be used to discharge validity requirements for pointers.
To state the operational semantics and type system we need a
few definitions. The program counters of a program pcs(P) are
{b.i | P = B0, . . . , Bm ∧ b ≤ m ∧ Bb = p; ι1; · · · ; ιn; c ∧ i ≤
n+1}. We write P(b) for Bb when P = B0, . . . , Bn and b ≤ n; if
P(b) = p; ι1; . . . ; ιm; c then P(b.n) is p when n = 0, and ιn when
1 ≤ n ≤ m and c when n = m + 1. The edges of a program P,
edges(P), are as follows. The entry edge is (−1, 0). If P(n) ends
in [x1 : τ1, x2 : τ2] if x3 rop x4 goto n′
then there are edges
(n, n+1), called the fall-through edge, and (n, n′
), called the taken
edge. If P(n) ends in goto n′
then there is an edge (n, n′
). For a
given P and n2 the edges (n1, n2) ∈ edges(P) are numbered from
zero in the order given by n1; edgeP (n1, n2) is this number, also
called the incoming edge number of (n1, n2) into n2.
Operational Semantics A program P is started in the state
(P, ∅, 0, 0.0). The reduction relation that maps one state to the
next is given in Figure 11. Note that the third component of a pro-
POPL ’06 Submission 5 2005/11/15
gram state tracks which incoming edge led to the current program
counter—initially this is the entry edge (−1, 0), and is updated by
transfers. It is used by phi instructions to select the correct variable.
The notation p[i] denotes x1 := x1i, . . . , xn := xni when p =
x1 : τ1 := φ(x11, . . . , x1m), . . . , xn : τn := φ(xn1, . . . , xnm).
A program terminates when in a state of the form (P, L, n, pc)
where P(pc) = halt. A program state is stuck if it is irreducible
and not a terminal state. Stuck states all represent type errors that
the type system should prevent. Note that the array creation opera-
tion must handle negative sizes. Our implementation would throw
an exception, but since the core language does not have exceptions,
it simply creates a zero length array if a negative size is requested.
In the operational semantics, the proof type has the single in-
habitant true, upon which no interesting operations are defined.
Proofs in this sense are equivalent to unit values for which non-
escaping occurrences can be trivially erased when moving to an
untyped setting. This “proof erasure” property is precisely analo-
gous to the “coercion erasure” property of the coercion language of
Vanderwaart et al. [23]. In practice, uses of proof variables in the
STARJIT compiler are restricted such that all proof terms can be
elided during code generation and consequently impose no over-
head at run time. While we believe that it would be straightforward
to formalize the syntactic restrictions that make this possible, we
choose for the sake of simplicity to leave this informal here.
Type System The type system has two components: the SSA
property and a set of typing judgements. The SSA property ensures
both that every variable is assigned to at most once in the program
text (the single assignment property) and that all uses of variables
are dominated by definitions of those variables. In a conventional
type system, these properties are enforced by the typing rules. In
particular, the variables that are listed in the context of the typing
judgement are the ones that are in scope. For SSA IRs, it is more
convenient to check these properties separately.
The type checker must ensure that during execution each use of
a variable is preceded by an assignment to that variable. Since the i-
th variable of a phi instruction is used only if the i-th incoming edge
was used to get to the block, and the proof variables in an if transfer
are assigned only on particular out-going edges, we give a rather
technical definition of points at which variables are assigned or
used. These points are such that a definition point dominating a use
point implies that assignment will always precede use. These points
are based on an unconventional notion of control-flow graph, to
avoid critical edges which might complicate our presentation. For
a program P with blocks 0 to m, the control-flow graph consists
of the nodes {0, . . . , m} ∪ edges(P) and edges from each original
node n to each original edge (n, n′
) and similarly from (n, n′
)
to n′
. The definition/use points, du(P), are pcs(P) ∪ {b.0.i |
P(b.0) = p0, . . . , pn ∧ 0 ≤ i ≤ n} ∪ {e.i | e ∈ edges(P) ∧ i ∈
{0, 1}}.
Figure 13 gives the formal definition of dominance, defini-
tion/use points, and the SSA property.
The syntax of types is:
Types τ ::= int | array(τ) | ptr?hτi | S(x) | pf(F )
Facts F ::= e1 rop e2 | F1 ∧ F2
Fact Exps. e ::= i | x | len(x) | e1 bop e2 | x@e
Environments Γ ::= x : τ
The type ptr?hτi is given to pointers that, if valid, point to values
with type τ (the ? indicates that they might not be valid). The
singleton type S(x) is given to things that are equal to x. The
type pf(F ) is given to proof variables that contain a proof of the
fact F. Facts include arithmetic comparisons and conjunction. Fact
expressions include integers, variables, array lengths, arithmetic
operations, and a subscript expression—the fact expression x@e
stands for a pointer that points to the element at index e of array x.
Judgement Meaning
Γ ⊢ τ1 ≤ τ2 τ1 is a subtype of τ2 in Γ
⊢ F1 =⇒ F2 F1 implies F2
Γ ⊢ p p is safe in environment Γ
Γ ⊢P ι ι is safe in environment Γ
Γ ⊢ c c is safe in environment Γ
⊢P τ at du τ well-formed type at du in P
⊢P Γ environment Γ well-formed in P
⊢ P P is safe
Figure 12. Typing judgements
The judgements of the type system are given in figure 12. Most
of the typing rules are given in Figure 14. Typing environments
Γ state the types that variables are supposed to have. The rules
check that when assignments are made to a variable, the type of the
assigned value is compatible with the variable’s type. For example,
the judgement Γ ⊢ int ≤ Γ(x) in the rule for x : τ := i checks
that integers are compatible with the type of x. The rules also check
that uses of a variable have a type compatible with the operation.
For example, the rule for load expects a proof that the pointer, x2,
is valid, so the rule checks that x3’s type Γ(x3) is a subtype of
pf(x@0≤x2∧x2<x@len(x)) for some x. It is this check along with the
rules for proof value generation and the SSA property that ensure
that x2 is valid.
Given these remarks, the only other complicated rule is for phi
instructions. In a loop a phi instruction might be used to combine
two indices, and the compiler might use another phi instruction to
combine the proofs that these indices are in bounds. For example,
consider this sequence:
x1 : int := φ(x2, x3)
y1 : pf(0≤x1) := φ(y2, y3)
where y2 : pf(0≤x2) and y3 : pf(0≤x3). Here the types for y1,
y2, and y3 are different and in some sense incompatible, but are
intuitively the correct types. The rule for phi instructions allows
this typing. In checking that y2 has a compatible type, the rule
substitutes x2 for x1 in y1’s type to get pf(0≤x2), which is the type
that y2 has; similarly for y3.
For a program P that satisfies the SSA property, every variable
mentioned in the program has a unique definition point, and that
definition point is decorated with a type. Let vt(P) denote the
environment formed from extracting these variable/type pairs. A
program P is well formed (⊢ P) if:
1. P satisfies the SSA property,
2. ⊢P vt(P),
3. vt(P) ⊢ p for every p in P,
4. vt(P) ⊢P ι for every instruction ι in P, and
5. vt(P) ⊢ c for every transfer c in P.
The type system is safe:
THEOREM 1 (Type Safety).
If ⊢ P and (P, ∅, 0, 0.0) 7→∗
S then S is not stuck.
A proof of this theorem is given in appendix A. The proof takes
the standard high-level form of showing preservation and progress
lemmas, as well as some lemmas particular to an SSA language.
It is important to note that safety of the type system is contingent
on the soundness of the decision procedure for ⊢ F1 =⇒ F2.
In the proof, a judgement corresponding to truth of facts in an
environment is given. In this setting, the assumption of logical
soundness corresponds to the restriction that in any environment
in which F1 is true, F2 is also true.
POPL ’06 Submission 6 2005/11/15
Defs and Uses:
If P(b.i) = x : τ := r then program counter b.i defines x, furthermore, b.i is a use of the ys where r has the following forms:
y | newarray(y1, y2) | len(y) | base(y) | y1 bop y2 | ld(y1) [y2] | pffact(y) | pfand(y)
If P(b.i) = (p0, . . . , pn) and pj = xj : τj := φ(yj1, . . . , yjm) then b.i.j defines each xj and ek.1 uses each yjk where ek is the k-th incoming edge
of b. If P(b.i) = [x1 : τ1, x2 : τ2] if y1 rop y2 goto n then e1.0 defines x1 and e2.0 defines x2 where e1 and e2 are the fall-through and taken edges
respectively, and b.i uses y1 and y2. If x has a unique definition/use point in P that defines it, then defP (x) is this point.
Dominance:
• In program P, node n dominates node m, written domP (n, m), if every path in the control-flow graph of P from (−1, 0) to m includes n.
• In program P, definition/use point n1.i1 strictly dominates definition/use point n2.i2, written sdomP (n1.i1, n2.i2) if n1 = n2 and i1 < i2 (here i1 or
i2 might be a dotted pair 0.j, so we take this inequality to be lexicographical ordering) or n1 6= n2 and domP (n1, n2).
Single Assignment:
A program satisfies the single-assignment property if every variable is defined by at most one definition/use point in that program.
In Scope:
A program P satisfies the in-scope property if for every definition/use point du1 that uses a variable there is a definition/use point du2 that defines that
variable and sdomP (du2, du1).
SSA:
A program satisfies the Single Static Assignment (SSA) property if it satisfies the single-assignment and in-scope properties. Note that a program that satisfies
SSA has a unique definition for each variable mentioned in the program.
Figure 13. SSA definitions
⊢P τ at du ⊢P Γ
fv(τ) ⊆ inscopeP (du)
⊢P τ at du
⊢P τ at defP (x)
⊢P x : τ
Γ ⊢ τ1 ≤ τ2 ⊢ F1 =⇒ F2
Γ ⊢ int ≤ int
Γ ⊢ τ1 ≤ τ2
Γ ⊢ array(τ1) ≤ array(τ2)
Γ ⊢ τ1 ≤ τ2
Γ ⊢ ptr?hτ1i ≤ ptr?hτ2i
Γ ⊢ S(x) ≤ S(x) Γ ⊢ S(x) ≤ Γ(x)
⊢ F1 =⇒ F2
Γ ⊢ pf(F1) ≤ pf(F2)
Γ ⊢ τ1 ≤ τ2 Γ ⊢ τ2 ≤ τ3
Γ ⊢ τ1 ≤ τ3
The judgement ⊢ F1 =⇒ F2 is some appropriate decision procedure for our fact language.
Γ ⊢ p Γ ⊢P ι Γ ⊢ c
Γ ⊢ S(xij) ≤ Γ(xi){x1, . . . , xn := x1j, . . . , xnj}
Γ ⊢ x1 : τ1 := φ(x11, . . . , x1m), . . . , xn : τn := φ(xn1, . . . , xnm)
Γ ⊢ int ≤ Γ(x)
Γ ⊢P x : τ := i
Γ ⊢ S(x2) ≤ Γ(x1)
Γ ⊢P x1 : τ := x2
Γ ⊢ Γ(x2) ≤ int Γ ⊢ array(Γ(x3)) ≤ Γ(x1)
Γ ⊢P x1 : τ := newarray(x2, x3)
Γ ⊢ Γ(x2) ≤ array(τ2) Γ ⊢ int ≤ Γ(x1)
Γ ⊢P x1 : τ := len(x2)
Γ ⊢ Γ(x2) ≤ array(τ2) Γ ⊢ ptr?hτ2i ≤ Γ(x1)
Γ ⊢P x1 : τ := base(x2)
Γ ⊢ Γ(x2) ≤ int Γ ⊢ Γ(x3) ≤ int Γ ⊢ int ≤ Γ(x1)
Γ ⊢P x1 : τ := x2 bop x3
Γ ⊢ Γ(x2) ≤ ptr?hτ2i Γ ⊢ Γ(x3) ≤ int Γ ⊢ ptr?hτ2i ≤ Γ(x1)
Γ ⊢P x1 : τ := x2 bop x3
Γ ⊢ Γ(x2) ≤ ptr?hτ2i Γ ⊢ Γ(x3) ≤ pf(x@0≤x2∧x2<x@len(x)) Γ ⊢ τ2 ≤ Γ(x1)
Γ ⊢P x1 : τ := ld(x2) [x3]
Γ ⊢ pf(deffactP (x2)) ≤ Γ(x1)
Γ ⊢P x1 : τ := pffact(x2)
Γ ⊢ Γ(y1) ≤ pf(F1) · · · Γ ⊢ Γ(yn) ≤ pf(Fn) Γ ⊢ pf(F1∧···∧Fn) ≤ Γ(x1)
Γ ⊢P x : τ := pfand(y1, . . . , yn)
Γ ⊢ Γ(x3) ≤ int Γ ⊢ Γ(x4) ≤ int Γ ⊢ pf(¬(x3 rop x4)) ≤ Γ(x1) Γ ⊢ pf(x3 rop x4) ≤ Γ(x2)
Γ ⊢ [x1 : τ1, x2 : τ2] if x3 rop x4 goto n
Γ ⊢ goto n Γ ⊢ halt
deffactP (x) The fact deffactP (x) depends upon the defining instruction of x in P, and is given by these rules:
deffactP (x : τ := i) = x=i
deffactP (x : τ := len(x′)) = x=len(x′)
deffactP (x : τ := base(x′)) = x = x′@0
deffactP (x : τ := x1 bop x2) = x=x1 bop x2
Figure 14. Typing rules
POPL ’06 Submission 7 2005/11/15
The typing rules presented are for the most part syntax-directed,
and can be made algorithmic. A consideration is that the rule for
load must determine the actual array variable, which is not apparent
from the conclusion. In general, the decision prodecure only needs
to verify that the rule holds for one of the arrays available at that
program point. In practice, the correct array can be inferred by ex-
amining the type of the proof variable. We believe that judgements
on facts may be efficiently decided by an integer linear program-
ming tool such as the Omega Calculator [21] with two caveats.
First, such tools reason over Z rather than 32- or 64-bit integers.
Second, they restrict our fact language for integer relations (and,
thus, compiler reasoning) to affine expressions. This is, however,
sufficient to capture current STARJIT optimizations.
4. Compiler optimizations
In this section we examine compiler optimizations in the context of
the core language. We demonstrate how an optimizing compiler can
preserve both proof variables and their type information. We argue
that our ideas greatly simplify this process. In previous work, an im-
plementer would need to modify each optimization to update safety
information. In our representation, we leverage existing compiler
infrastructure to do the bulk of the work. In particular, most control-
flow or data-flow optimizations require virtually no changes at all.
Others that incorporate algebraic properties only need to be modi-
fied to record the compiler’s reasoning. In the next section we will
discuss how these ideas can be extended from the core language to
full Java.
In general, there are two ways in which an optimization can
maintain the correctness of the proofs embedded in the program.
First, it can apply the transformation to both computation and proof
simultaneously. This is sufficient for the majority of optimizations.
Second, it can create new proofs for the facts provided by the
original computation. As we show below, this is necessary for
the few optimizations that infer new properties that affect safety.
In the rest of this section we show how these general principles
apply to individual compiler optimizations on a simple example.
For this example, we show how to generate a low-level intermediate
representation that contains safety information and how to preserve
this information through several compiler optimizations, such as
loop invariant code motion, common subexpression elimination,
array bounds check elimination, strength reduction of array element
pointer, and linear function test replacement.
The example we will consider, in pseudo code, is:
for (i=0; i<a.length; i++) {
· · · = a[i];
}
Where we assume that a is a non-null integer array, that a is not
modified in the loop, and that the pseudo code array subscripting
has an implicit bounds check. Although this example does not
reflect the full complexity of Java, it is sufficient to illustrate the
main ideas of propagating safety information through the compiler
optimizations. Section 5 discusses additional issues in addressing
full Java.
The first compilation step for our example lowers the program
into a low-level representation suitable for optimization, as shown
in Figure 15. In our system, lowering generates instructions that ex-
press the computation and any required proofs of the computation’s
safety. For example, a typical compiler would expand an array ele-
ment access a[i] into the following sequence: array bounds checks,
computation of the array element address, and a potentially unsafe
load from that address. In our system, the compiler also generates
proof variables that show that the array index i is within the ar-
ray bounds (q4 for the lower bound and q6 for the upper bound)
and that the load accesses an element i of the array a (proof vari-
i1 : int :=0
uB : int :=len(a)
LOOP :
i2 : int :=φ(i1, i3)
[q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT
aLen : int :=len(a)
q3 : pf(aLen=len(a)) :=pffact(aLen)
q4 : pf(0≤i2) :=checkLowerBound(i2, 0)
q5 : pf(i2<aLen) :=checkUpperBound(i2, aLen)
q6 : pf(i2<len(a)) :=pfand(q3, q5)
aBase : ptr?hinti :=base(a)
q7 : pf(aBase=a@0) :=pffact(aBase)
addr : ptr?hinti :=aBase+i2
q8 : pf(addr=aBase+i2) :=pffact(addr)
q9 : pf(addr=a@i2) :=pfand(q7, q8)
q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9)
val : int :=ld(addr) [q10]
. . . : . . . :=val
i3 : int :=i2+1
goto LOOP
EXIT :
. . .
Figure 15. Low-level representation for array load in loop
i1 : int :=0
uB : int :=len(a)
q3 : pf(uB=len(a)) :=pffact(uB)
aBase : ptr?hinti :=base(a)
q7 : pf(aBase=a@0) :=pffact(aBase)
LOOP :
i2 : int :=φ(i1, i3)
[q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT
q4 : pf(0≤i2) :=checkLowerBound(i2, 0)
q5 : pf(i2<uB) :=checkUpperBound(i2, uB)
q6 : pf(i2<len(a)) :=pfand(q3, q5)
addr : ptr?hinti :=aBase+i2
q8 : pf(addr=aBase+i2) :=pffact(addr)
q9 : pf(addr=a@i2) :=pfand(q7, q8)
q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9)
val : int :=ld(addr) [q10]
. . . : . . . :=val
i3 : int :=i2+1
goto LOOP
EXIT :
. . .
Figure 16. IR after CSE and loop invariant code motion
able q9). The conjunction of these proofs is sufficient to type check
the load instruction according to the typing rules in Figure 14. The
proof variables are generated by the explicit array bounds checks
(which we use as syntactic sugar for the branches that transfer con-
trol to a halt instruction if the bounds check fails) and by pffact
and pfand statements that encode arithmetic properties of the ad-
dress computation as the types of proof variables.
Next, we take the example in Figure 15 through several common
compiler optimizations that are employed by STARJIT to generate
efficient code for loops iterating over arrays (Figures 16 - 19). The
result is highly-optimized code with an embedded proof of program
safety.
We start, in Figure 16, by applying several basic data-flow op-
timizations such as CSE, dead code elimination, and loop invari-
ant code motion. An interesting property of these optimizations
in our system is that they require no modification to preserve the
POPL ’06 Submission 8 2005/11/15
i1 : int :=0
q11 : pf(i1=0) :=pffact(i1)
uB : int :=len(a)
q3 : pf(uB=len(a)) :=pffact(uB)
aBase : ptr?hinti :=base(a)
q7 : pf(aBase=a@0) :=pffact(aBase)
LOOP :
i2 : int :=φ(i1, i3)
q4 : pf(0≤i2) :=φ(q11, q13)
[q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT
q6 : pf(i2<len(a)) :=pfand(q3, q1)
addr : ptr?hinti :=aBase+i2
q8 : pf(addr=aBase+i2) :=pffact(addr)
q9 : pf(addr=a@i2) :=pfand(q7, q8)
q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9)
val : int :=ld(addr) [q10]
. . . : . . . :=val
i3 : int :=i2+1
q12 : pf(i3=i2+1) :=pffact(i3)
q13 : pf(0≤i3) :=pfand(q4, q12)
goto LOOP
EXIT :
. . .
Figure 17. IR after bound check elimination
safety proofs. They treat proof variables identically to other terms,
and, thus, are automatically applied to both the computation and
the proofs. For example, common subexpression elimination and
copy propagation replace all occurrences of aLen with uB, includ-
ing those that occur in proof types. The type of the proof variable
q3 is updated to match its new definition pffact(uB).
In Figure 17, we illustrate array bounds check elimination. In
the literature [4], this optimization is typically formulated to re-
move redundant bounds checks without leaving any trace of its rea-
soning in the program. In such an approach, a verifier must effec-
tively repeat the optimization reasoning to prove program safety. In
our system, an optimization cannot eliminate an instruction that de-
fines a proof variable without constructing a new definition for that
variable or removing all uses of that variable. Intuitively, the com-
piler must record in a new definition its reasoning about why the
eliminated instruction was redundant. Consider the bounds checks
in Figure 16. The lower bound check that verifies that 0≤i2 is
redundant because i2 is a monotonically increasing variable with
the initial value 0. Formally, the facts that i1=0, i2=φ(i1, i3) and
i3=i2+1 imply that 0≤i2. This reasoning is recorded in the trans-
formed program through a new definition of the proof variable q4
and the additional proof variables q11 and q13. We use SSA to con-
nect these proofs at the program level. The upper bound check that
verifies that i2<len(a) (proof variable q5) is redundant because
the if statement guarantees the same condition (proof variable q1).
Because the new proof for the fact q5 is already present in the pro-
gram, the compiler simply replaces all uses of of q5 with q1.
In Figure 18, we perform operator strength reduction (OSR) [9]
to find a pointer that is an affine expression of a monotonically in-
creasing or decreasing loop index variable and to convert it into an
independent induction variable. In our example, OSR eliminates i
from the computation of addr by incrementing it directly. Because
variable addr is used in the q8 := pffact(addr) statement, the
compiler cannot modify the definition of addr without also mod-
ifying the definition of q8 (otherwise, the transformed program
would not type check). Informally, the compiler must reestablish
the proof that the fact trivially provided by the original definition
still holds. In our system, OSR is modified to construct a new proof
for the fact trivially implied by the original pointer definition by
i1 : int :=0
q11 : pf(i1=0) :=pffact(i1)
uB : int :=len(a)
q3 : pf(uB=len(a)) :=pffact(uB)
aBase : ptr?hinti :=base(a)
q7 : pf(aBase=a@0) :=pffact(aBase)
addr1 : ptr?hinti :=aBase+i1
q14 : pf(addr1=aBase+i1) :=pffact(addr1)
LOOP :
i2 : int :=φ(i1, i3)
q4 : pf(0≤i2) :=φ(q11, q13)
addr2 : ptr?hinti :=φ(addr1, addr3)
q8 : pf(addr2=aBase+i2) :=φ(q14, q16)
[q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT
q6 : pf(i2<len(a)) :=pfand(q3, q1)
q9 : pf(addr2=a@i2) :=pfand(q7, q8)
q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9)
val : int :=ld(addr2) [q10]
. . . : . . . :=val
i3 : int :=i2+1
q12 : pf(i3=i2+1) :=pffact(i3)
addr3 : ptr?hinti :=addr2+1
q15 : pf(addr3=addr2+1) :=pffact(addr3)
q13 : pf(0≤i3) :=pfand(q4, q12)
q16 : pf(addr3=aBase+i3) :=pfand(q8, q12, q15)
goto LOOP
EXIT :
. . .
Figure 18. IR after strength reduction of element address
uB : int :=len(a)
q3 : pf(uB=len(a)) :=pffact(uB)
aBase : ptr?hinti :=base(a)
q7 : pf(aBase=a@0) :=pffact(aBase)
addr1 : ptr?hinti :=aBase
q14 : pf(addr1=aBase) :=pffact(addr1)
addrUB : ptr?hinti :=aBase+uB
q17 : pf(addrUB=aBase+uB) :=pffact(addrUB)
LOOP :
addr2 : ptr?hinti :=φ(addr1, addr3)
q4 : pf(aBase≤addr2) :=φ(q14, q13)
[q1 : pf(addr2<addrUB), q2 : . . .] := if addrUB≤addr2
goto EXIT
q6 : pf(addr2<aBase+len(a)) :=pfand(q3, q1, q17)
q10 : pf(a@0≤addr2<a@len(a)) :=pfand(q4, q6, q7)
val : int :=ld(addr2) [q10]
. . . : . . . :=val
addr3 : ptr?hinti :=addr2+1
q15 : pf(addr3=addr2+1) :=pffact(addr3)
q13 : pf(aBase≤addr3) :=pfand(q4, q15)
goto LOOP
EXIT :
. . .
Figure 19. IR after linear function test replacement
induction on that fact. Again, we leverage SSA to establish the new
proof. In this case, q8 : pf(addr2=aBase+i2) is defined by the phi in-
struction that merges proof variables q14 : pf(addr1=aBase+i1) and
q16 : pf(addr3=aBase+i3).
POPL ’06 Submission 9 2005/11/15
Finally, we illustrate linear function test replacement (LFTR) [9]
in Figure 19. 1
Classical LFTR replaces the test uB≤i2 in the branch
by a new test addrUB≤addr2. If our program contained no proof
variables, this would allow the otherwise unused base variable i
to be removed from the loop. We augment the usual LFTR pro-
cedure, which rewrites occurrences of the base induction variable
i2 in loop exit tests (and exits) in terms of the derived induction
variable addr2, to also rewrite occurrences of i2 in the types of
proof variables. Finally, to eliminate the original induction variable
altogether, the compiler must replace the inductive proofs on the
original variable (expressed through φ instructions) with proofs in
terms of the derived induction variable. In this case, the compiler
must replace the proof that 0≤i2 (established by q11 and q12) with
one that proves aBase≤addr2 (established by q14 and q15). Af-
ter the replacement, the loop induction variable i and any proof
variables that depend upon it are no longer live in the loop, so all
definitions of the variable can be removed. The compiler must re-
move the proof variables whose types reduce to tautologies and
apply further CSE to yield Figure 19.
5. Extensions
Our core language can easily be extended to handle other interest-
ing aspects of Java and CLI. In this section we describe several of
these extensions.
Firstly, we can handle object-model lowering through the use
of our singleton types. Consider an invoke virtual operation. It is
typically lowered into three operations: load the virtual dispatch
table (vtable), load the method pointer from the vtable, call the
method pointer passing the object as an additional argument. In
our system, these operations would look like this:
x : SomeClass := · · ·
t1 : vtable(x) := vtable(x)
t2 : (S(x), int) → int := method(foo : (int) → int, t1)
t3 : int := call(t2)(x, 10)
Here the method foo (taking an integer and returning an integer)
is being invoked on variable x. In the lowered code, variable t1
gets the dependent type vtable(x) meaning that it contains the
vtable from the object currently in x. Variable t2 gets the loaded
method pointer. From the type vtable(x), the typing rules can
determine a precise function type for this method pointer, namely
(S(x), int) → int, where the first argument must be x. The actual
call is the last operation, and here we pass x as an explicit argument.
Since x has type S(x), this operation type checks.
By using singleton types based on term variables, we achieve
a relatively simple type system and still avoid the well known
typing problems with the explicit “this” argument (see [12] and
references). The existing solutions to this typing problem have
much more complicated type systems, with one exception. Chen
and Tarditi [7] have a similarly simple type system for a lowered IR
for class-based object-oriented languages. Like our system, theirs
also has class names as types, and keeps around information about
the class hierarchy, fields, and methods. They also have existentials
with subclass bounds (type variables can be bounded above by a
class, and range over any subclass of that class). They use these
existentials to express the unknown runtime type of any given
object, and thus the type of the explicit “this” argument. They
also have a class representation function that maps class names
1 Note that the code resulting from LFTR is not typable in our core lan-
guage, since we do not allow conditional branches on pointers. Extending
the language to handle this is straightforward, but requires a total ordering
on pointer values which essentially requires moving to a heap-based seman-
tics. Note though that the fact language does permit reasoning about pointer
comparison, as used in the previous examples.
to a record type for objects in the class, and they have coercions
to convert between the two. These ideas could be adapted to our
system instead of our vtable types, and our vtable types could be
adapted to their type system. In summary, both systems are simpler
than existing, more foundational, object encodings. Theirs has type
variables and bounded existentials, ours has singleton types based
on term variables.
Java and CLI also allow null as a value in any class type, and at
runtime this null value must be checked and an exception thrown
before any invocation or field access on an object. We can use our
proof variable technique to track and ensure that these null checks
are done. We simply add a null constant to the fact expression lan-
guage. We can add an operation like p : pf(x6=null) := chknull(x)
to check that x is not null. If x is null then it throws an exception,
if not then it assigns a proof of x6=null to p. Similarly to array-
bounds check elimination, we can eliminate redundant null checks.
To handle exceptions we simply add explicit control flow for
them. Each potentially exception throwing operation will end a ba-
sic block and there will be edges coming out of the block corre-
sponding to exceptions that go to blocks corresponding to the ex-
ception handlers. An important point is that exceptions typically
occur before the assignment of the potentially exception throw-
ing operation, so like the conditional branches of our core lan-
guage, we must treat the definition point as occuring on the fall-
through edge rather than at the end of the basic block. So in both
x : τ := chknull(y) and x : τ := call(y)(y), the variable x is
assigned on the fall-through edge.
We can easily deal with stores to pointers by adding a store
operation of the form st(x, y) [p] where x holds the pointer, y
the value to store, and p a proof that x is valid. The type rule for
this operation is:
Γ ⊢ Γ(x) ≤ ptr?hτi Γ ⊢ Γ(y) ≤ τ
Γ ⊢ Γ(p) ≤ pf(z@0≤x∧x<z@len(z))
Γ ⊢P st(x, y) [p]
Modifying our formalisation and type soundness proof to accomo-
date stores would be straightforward.
Java and CLI have mutable covariant arrays, and thus require
array-store checks at runtime. In particular, when storing into an
array, the runtime must check that the object being stored is com-
patible with the runtime element type of the array (which could be
a subtype of the static element type). In our implementation we use
types of the form elem(x) to stand for the runtime element type of
array x. The load base operation on x actually returns something of
type ptr?helem(x)i. The array-store check produces a proof value
that can be used to prove that some other variable has type elem(x)
and we have a coercion to use the proof value to change the vari-
able’s type. The end of a lowered array store would look something
like this:
x : array(C) := · · ·
y : C := · · ·
· · ·
p1 : pf(x6=null∧x@0≤t∧t<x@len(x)) := · · ·
p2 : pf(y:elem(x)) := chkst(x, y)
st(t, retype(y, p2)) [p1]
One technicality is worth noting. In order to avoid circularities
between the type system and the fact language, and to avoid making
the fact language’s decision procedure mutually dependent upon
the subtype checker, we restrict the types that can appear in a fact
of the form x : τ to those that do not mention proof types.
Downcasts are similar to store checks, and we can treat them in
a similar way. A chkcast(x : C) operation checks that x is in type
C and returns a proof of this fact, otherwise it throws an exception.
The actual subtype checks performed at runtime in our implementa-
POPL ’06 Submission 10 2005/11/15
tion are generally done by the virtual machine itself, and the virtual
machine is not type checked by the type system of our JIT. How-
ever, we do partially inline this operation to include some common
fast cases, and to expose some parts to redundant elimination and
CSE. For example, if a object is null then it is in any reference type
and can be stored into any reference array or downcast to any ref-
erence type. Another example is comparing the vtable of an object
against the vtable of a specific class, if these are equal then that ob-
ject is in that class. Such comparisons produce facts in our system
of the form x=null or vtable(x)=vtable(C). We can simply
add axioms to our fact language like ⊢ x=null =⇒ x : C or
⊢ vtable(x)=vtable(C) =⇒ x : C.
6. Implementation Status
The current implementation of the STARJIT compiler generates
and maintains proof variables throughout its compilation process to
enable safe implementation of certain optimizations in the presence
of check elimination (to be described in a forthcoming paper). For
their initially designed role in optimizations, proof variables did not
require proof types: optimizations do not need to know the reason
an optimization was safe, but only its safety dependences. As such,
the current STARJIT representation is similar to that described in
Section 2 with some of the extensions in Section 5.
STARJIT implements all of the optimizations discussed in this
paper as well as more described in [1]. We modified each opti-
mization, if necessary, to correctly handle proof variables. Array
bounds check elimination and operator strength reduction required
the most significant modification, as described in Section 4. For
partial inlining of virtual machine type checking functions, as de-
scribed in Section 5, we updated the definition of proof variables to
established that a variable has the checked type. We also modified
method inlining to properly establish the type of inlined methods.
For each parameter of a method, we added a proof variable that es-
tablished that it had the correct type. When a method is compiled
independently, that proof variable is trivially defined at the method
entry (as parameter types to a method are guaranteed by the run-
time environment). When the method is inlined, the corresponding
proof variables must be defined by the calling method instead. As
method call operations require proof variables for each parameter
in our system, this information is readily available. Most optimiza-
tions, however, did not require significant changes for the reasons
outlined in this paper.
An early version of a type verifier which inferred proof types it-
self was implemented. This implementation was particularly help-
ful in finding bugs within STARJIT, but was insufficient for com-
plete verification of optimized code. In particular, the inference al-
gorithm was insufficient for some more complicated optimization
situations, such as the LFTR example (without proof type informa-
tion) in Section 4. We are confident that extending the compiler to
use precise proof types for proof variables will be straightforward,
using the framework developed in this paper.
7. Related Work
As far as we are aware, SafeTSA [24, 2] is the only other example
of a type-safe SSA representation in the literature. The motivation
of their work is rather different than ours. SafeTSA was designed
as an alternative to Java bytecode, whereas our representation is de-
signed to be a low-level intermediate language for a bytecode com-
piler. SafeTSA can represent certain optimizations, such as CSE
and limited check elimination, that Java bytecode does not. How-
ever, in our classification in Section 2, SafeTSA is a refinement-
style representation and, thus, cannot represent the effect of many
of the low-level optimizations we discuss here. For example, it can-
not represent the safety of check elimination based upon a previous
branch or the construction of an unsafe memory address as illus-
trated in Figure 7. On the other hand, we do not support their notion
of referential security: the property that a program must be safe by
construction.
While most of the work on certified code focuses on the final
machine code representation, there has been previous work on
intermediate representations that allow verification of the memory
safety of highly optimized machine level code. One of the major
differences between the various approaches lies in the degree to
which safety information is made explicit.
On the side of less explicit information are the SpecialJ com-
piler [8] and DTAL [26]. Both approaches record loop invariants,
but not explicit safety dependences. This makes verification harder
(all available invariants must be considered by the decision pro-
cedure), interferes with more optimizations (such as loop peeling)
than our approach, and makes removing dead invariants much more
difficult (because invariants never have explicit uses).
At the other end of the spectrum, there are other systems that not
only represent dependences explicitly as we do, but also record ex-
actly why the dependences imply safety for each instruction, using
proofs, instead of relying on a decision procedure during checking,
as in our system. The LTT system of Crary and Vanderwaart [10]
and the TSCB system of Shao et al. [22], developed independently,
both take this approach, albeit in the setting of a functional or
mostly-functional language. Both systems are designed around the
idea of incorporating a logic into a type theory, in order to combine
the benefits of proof-carrying code [19] with the convenience of
a type system. LTT and TSCB adopt the linear logical framework
LLF and the Calculus of Inductive Constructions, respectively, as
their proof languages. Incorporating a proof system also gives them
more flexibility, as they can express a variety of properties within a
single framework.
The lack of explicit proofs in the representation forces us to
use a decision procedure during typechecking. This limits us to
decidable properties, and may be less suited for certified code
applications where the added complexity of a decision procedure
in the verifier may be undesirable.
On the other hand, a system such as ours is much more suited
to use in the internals of an optimizing compiler. For the limited
use that we need proofs for—to verify the correctness of checks
which are eliminated by a real optimizing compiler—we can get
away with a vastly simpler system, one that imposes much less of
a burden on the compiler than more syntactically heavy systems.
Moreover, for applications of certified code, we believe that it
should be possible to take optimized intermediate code in the style
presented here and translate it, as part of code generation, to a
more explicit form in the style of LTT or TSCB, thereby reaping
the benefits of both approaches, perhaps by following the Special
J model of using a proof generating theorem prover. However, this
remains future work.
Finally, our proof variables are also similar to the Jalapeño Java
system’s condition registers as described in [6, 14]. Both are mech-
anisms to represent control-flow information as abstract value de-
pendences. Their usage, however, is more limited. Condition regis-
ters are not used to express general safety information or to support
verification of code. Instead, they are used by the compiler to model
control flow between a check operation and all (rather than just po-
tentially unsafe) instructions that follow it. Jalapeño uses condition
registers to collapse control flow due to exceptions into a single ex-
tended block and, in that block, to prevent instruction reordering
that would violate control flow dependences.
8. Conclusions
This paper has shown a typed low-level program representation
that preserves memory safety dependences in highly-optimizing
POPL ’06 Submission 11 2005/11/15
type-preserving compilers. Our representation encodes safety de-
pendences as first-class term-level proof variables that capture the
essential memory-safety dependences in the program without artifi-
cially constraining optimizations—previous approaches that piggy-
back safety dependence on top of value dependence inhibit opti-
mization opportunities. Our representation encodes proofs of mem-
ory safety as dependent types associated with proof variables. Ex-
perience implementing this representation in the STARJIT com-
piler has demonstrated that a highly-optimizing Java JIT compiler
can easily generate and maintain this representation in the pres-
ence of aggressive SSA-based optimizations such as bounds check
elimination, value numbering, strength reduction, linear function
test replacement, and others. Using explicit proof values and proof
types, modern optimizing compilers for type-safe languages can
now generate provably safe yet low-level intermediate representa-
tions without constraining optimizations.
References
[1] ADL-TABATABAI, A.-R., BHARADWAJ, J., CHEN, D.-Y., GHU-
LOUM, A., MENON, V. S., MURPHY, B. R., SERRANO, M., AND
SHPEISMAN, T. The StarJIT compiler: A dynamic compiler for man-
aged runtime environments. Intel Technology Journal 7, 1 (February
2003).
[2] AMME, W., DALTON, N., VON RONNE, J., AND FRANZ, M.
SafeTSA: a type safe and referentially secure mobile-code repre-
sentation based on static single assignment form. In Proceedings of
the ACM SIGPLAN 2001 conference on Programming language de-
sign and implementation (Snowbird, UT, USA, 2001), pp. 137–147.
[3] BILARDI, G., AND PINGALI, K. Algorithms for computing the static
single assignment form. J. ACM 50, 3 (2003), 375–425.
[4] BODÍK, R., GUPTA, R., AND SARKAR, V. ABCD: Eliminating
array bounds checks on demand. In Proceedings of the ACM
SIGPLAN 2000 conference on Programming language design and
implementation (Vancouver, British Columbia, Canada, 2000),
pp. 321–333.
[5] BRIGGS, P., COOPER, K. D., AND SIMPSON, L. T. Value
numbering. Software—Practice and Experience 27, 6 (June 1996),
701–724.
[6] CHAMBERS, C., PECHTCHANSKI, I., SARKAR, V., SERRANO,
M. J., AND SRINIVASAN, H. Dependence analysis for Java. In
Proceedings of the 12th International Workshop on Languages and
Compilers for Parallel Computing (1999), vol. 1863 of Lecture Notes
in Computer Science, pp. 35–52.
[7] CHEN, J., AND TARDITI, D. A simple typed intermediate language
for object-oriented languages. In Proceedings of the 32nd Annual
ACM Symposium on Principles of Programming Languages (Long
Beach, CA, USA, Jan. 2005), ACM Press, pp. 38–49.
[8] COLBY, C., LEE, P., NECULA, G. C., BLAU, F., PLESKO, M., AND
CLINE, K. A certifying compiler for Java. In PLDI ’00: Proceedings
of the ACM SIGPLAN 2000 conference on Programming language
design and implementation (New York, NY, USA, 2000), ACM Press,
pp. 95–107.
[9] COOPER, K. D., SIMPSON, L. T., AND VICK, C. A. Operator
strength reduction. ACM Transactions on Programming Languages
and Systems (TOPLAS) 23, 5 (September 2001), 603–625.
[10] CRARY, K., AND VANDERWAART, J. An expressive, scalable type
theory for certified code. In ACM SIGPLAN International Conference
on Functional Programming (Pittsburgh, PA, 2002), pp. 191–205.
[11] CYTRON, R., FERRANTE, J., ROSEN, B., WEGMAN, M., AND
ZADECK, K. An efficient method of computing static single
assignment form. In Proceedings of the Sixteenth Annual ACM
Symposium on the Principles of Programming Languages (Austin,
TX, Jan. 1989).
[12] GLEW, N. An efficient class and object encoding. In Proceedings
of the 15th ACM SIGPLAN conference on Object-oriented program-
ming, systems, languages (Minneapolis, MN, USA, Oct. 2000), ACM
Press, pp. 311–324.
[13] GROSSMAN, D., AND MORRISETT, J. G. Scalable certification
for typed assembly language. In TIC ’00: Selected papers from the
Third International Workshop on Types in Compilation (London, UK,
2001), Springer-Verlag, pp. 117–146.
[14] GUPTA, M., CHOI, J.-D., AND HIND, M. Optimizing Java programs
in the presence of exceptions. In Proceedings of the 14th European
Conference on Object-Oriented Programming - ECOOP ’00 (Lecture
Notes in Computer Science, Vol. 1850) (June 2000), Springer-Verlag,
pp. 422–446.
[15] IGARASHI, A., PIERCE, B., AND WADLER, P. Featherweight Java:
A minimal core calculus for Java and GJ. ACM Transactions on
Programming Languages and Systems (TOPLAS) 23, 3 (May 2001),
396–560. First appeared in OOPSLA, 1999.
[16] KNOOP, J., RÜTHING, O., AND STEFFEN, B. Lazy code motion.
In Proceedings of the SIGPLAN ’92 Conference on Programming
Language Design and Implementation (San Francisco, CA, June
1992).
[17] MORRISETT, G., CRARY, K., GLEW, N., GROSSMAN, D.,
SAMUELS, R., SMITH, F., WALKER, D., WEIRICH, S., AND
ZDANCEWIC, S. TALx86: A realistic typed assembly language. In
Second ACM SIGPLAN Workshop on Compiler Support for System
Software (Atlanta, Georgia, 1999), pp. 25–35. Published as INRIA
Technical Report 0288, March, 1999.
[18] MORRISETT, G., WALKER, D., CRARY, K., AND GLEW, N. From
System F to typed assembly language. ACM Transactions on
Programming Languages and Systems (TOPLAS) 21, 3 (May 1999),
528—569.
[19] NECULA, G. Proof-carrying code. In POPL1997 (New York, New
York, January 1997), ACM Press, pp. 106–119.
[20] NECULA, G. C., AND LEE, P. The design and implementation
of a certifying compiler. In PLDI ’98: Proceedings of the ACM
SIGPLAN 1998 conference on Programming language design and
implementation (New York, NY, USA, 1998), ACM Press, pp. 333–
344.
[21] PUGH, W. The Omega test: A fast and practical integer programming
algorithm for dependence analysis. In Proceedings of Supercomput-
ing ’91 (Albuquerque, NM, Nov. 1991).
[22] SHAO, Z., SAHA, B., TRIFONOV, V., AND PAPASPYROU, N. A
type system for certified binaries. In Proceedings of the 29th Annual
ACM Symposium on Principles of Programming Languages (January
2002), ACM Press, pp. 216–232.
[23] VANDERWAART, J. C., DREYER, D. R., PETERSEN, L., CRARY,
K., AND HARPER, R. Typed compilation of recursive datatypes.
In Proceedings of the TLDI 2003: ACM SIGPLAN International
Workshop on Types in Language Design and Implementation (New
Orleans, LA, January 2003), pp. 98–108.
[24] VON RONNE, J., FRANZ, M., DALTON, N., AND AMME, W.
Compile time elimination of null- and bounds-checks. In 3rd
Workshop on Feedback-Directed and Dynamic Optimization (FDDO-
3) (December 2000).
[25] WALKER, D., CRARY, K., AND MORISETT, G. Typed memory man-
agement via static capabilities. ACM Transactions on Programming
Languages and Systems (TOPLAS) 22, 4 (July 2000), 701–771.
[26] XI, H., AND HARPER, R. Dependently typed assembly language.
In International Conference on Functional Programming (September
2001), pp. 169–180.
POPL ’06 Submission 12 2005/11/15
Γ ⊢P v : τ in L at du L ⊢P e is v at du L ⊢P F at du
x ∈ dom(L) x ∈ inscopeP (du)
Γ ⊢P L(x) : S(x) in L at du Γ ⊢P i : int in L at du
Γ ⊢P v : τ in L at du
Γ ⊢P hvi : array(τ) in L at du
Γ ⊢P v : τ in L at du
Γ ⊢P hvi@i : ptr?hτi in L at du
L ⊢P F at du
Γ ⊢P true : pf(F ) in L at du
Γ ⊢P v : τ1 in L at du Γ ⊢ τ1 ≤ τ2
Γ ⊢P v : τ2 in L at du
x ∈ dom(L) x ∈ inscopeP (du)
L ⊢P x is L(x) at du
L ⊢P x is hv0, . . . , vn−1i at du
L ⊢P len(x) is n at du
L ⊢P i is i at du
L ⊢P e1 is i1 at du L ⊢P e2 is i2 at du
L ⊢P e1bope2 is i1 bop i2 at du
L ⊢P e1 is i1 at du L ⊢P e2 is v@i2 at du
L ⊢P e1bope2 is v@(i1 bop i2) at du
L ⊢P e1 is v@i1 at du L ⊢P e2 is i2 at du
L ⊢P e1bope2 is v@(i1 bop i2) at du
L ⊢P x is hvi at du L ⊢P e is i at du
L ⊢P x@e is hvi@i at du
L ⊢P e1 is hv0, . . . , vni@i1 at du L ⊢P e2 is hv0, . . . , vni@i2 at du i1 rop i2
L ⊢P e1rope2 at du
L ⊢P e1 is i1 at du L ⊢P e2 is i2 at du i1 rop i2
L ⊢P e1rope2 at du
L ⊢P F1 at du L ⊢P F2 at du
L ⊢P F1 ∧ F2 at du
Γ ⊢P L : Γ′
@V ⊢ S
∀x ∈ V : Γ ⊢P L(x) : Γ′
(x) in L at defP (x)
Γ ⊢P L : Γ′
@V
⊢ P vt(P) = Γ
Γ ⊢P L : Γ@dfndAtP (pc, n)
n is an in-edge number for b where pc = b.i
pc ∈ pcs(P)
∀x ∈ dfndAtP (pc, n) if deffactP (x) = F then L ⊢P F at pc
⊢ (P, L, n, pc)
Figure 20. Typing for States, Environments, Values, and Facts
A. Appendix: Proof of Type Safety
A.1 Preliminaries
ASSUMPTION 1 (Logical Soundness). If L ⊢P F1 at du and ⊢ F1 =⇒ F2 then L ⊢P F2 at du.
Note that if domP (n1, n2) and domP (n2, n1) then n1 = n2. Hence strict dominance is asymetric. Thus dominance and strict dominance
induce a partial order that we can think of as a dominance tree rooted at (−1, 0) and (−1, 0).0 respectively.
Let inscopeP (du) = {x | sdomP (defP (x), du)}. The latter set is the variables we know are defined at the beginning of an instruction
(if du is the program counter). However, at the beginning of the phi instructions we also need variables in scope on the incoming edge to be
defined. Therefore, we define dfndAtP (b.0, e) to be inscopeP (e.1), dfndAtP (b.(i + 1), e) to be inscopeP (b.(i + 1)), and dfndAtP (b.i, n)
to be dfndAtP (b.i, e) where e is the n-th incoming edge to b.
The typing for states, environments, values, and facts appears in Figure 20.
A.2 Auxiliary lemmas
LEMMA 1. If L1(x) = L2(x) for all x ∈ inscopeP (du) and sdomP (du, du′
) or du = du′
then:
• If Γ ⊢P v : τ in L1 at du then Γ ⊢P v : τ in L2 at du′
.
• If L1 ⊢P F at du then L2 ⊢P F at du′
.
Proof: The proof is by induction on the typing derivation. In the case of the singleton value rule and the rule for the value of an expression
that is a variable, the variable in question has to be in scope for du, so L1 and L2 agree on its value and the variable is in scope for du′
.
COROLLARY 1 (Environment weakening).
• If L ⊢P e is ve at du and x /
∈ inscopeP (du) and then L{x := v} ⊢P e is ve at du
• If L ⊢P F at du and x /
∈ inscopeP (du) then L{x := v} ⊢P F at du
Proof: Follows immediately from lemma 1.
POPL ’06 Submission 13 2005/11/15
LEMMA 2. If Γ ⊢P L1 : Γ@inscopeP (du), x /
∈ inscopeP (du), L2 = L1{x := v} and Γ ⊢P v : Γ(x) in L2 at defP (x) then
Γ ⊢P L2 : Γ@inscopeP (du) ∪ {x}.
Proof: The result follows by the typing rule if Γ ⊢P L2(x) : Γ(x) in L2 at defP (x) for x ∈ inscopeP (du) ∪ {x}. For x in the
latter, the judgement holds by hypothesis. For x in the former, note that inscopeP (defP (x)) ⊆ inscopeP (du), so L1(y) = L2(y) for all
y ∈ inscopeP (defP (x)) and clearly L1(x) = L2(x). Thus the judgement holds by hypothesis and Lemma 1.
Note that subtyping is reflexive and transitive.
LEMMA 3 (Subtyping Inversion).
• If Γ ⊢ τ ≤ S(x) then τ = S(y) for some y.
• If Γ ⊢ S(x) ≤ τ then either τ = S(x) or Γ ⊢ Γ(x) ≤ τ.
• If Γ ⊢ array(τ1) ≤ array(τ2) then Γ ⊢ τ1 ≤ τ2.
• If Γ ⊢ ptr?hτ1i ≤ ptr?hτ2i then Γ ⊢ τ1 ≤ τ2.
• If Γ ⊢ pf(F1) ≤ pf(F2) then ⊢ F1 =⇒ F2.
• The following are not derivable: Γ ⊢ int ≤ array(τ), Γ ⊢ int ≤ ptr?hτi, Γ ⊢ int ≤ S(x), Γ ⊢ int ≤ pf(F ),
Γ ⊢ array(τ) ≤ int, Γ ⊢ array(τ) ≤ ptr?hτ′
i, Γ ⊢ array(τ) ≤ S(x), Γ ⊢ array(τ) ≤ pf(F ), Γ ⊢ ptr?hτi ≤ int,
Γ ⊢ ptr?hτi ≤ array(τ′
), Γ ⊢ ptr?hτi ≤ S(x), Γ ⊢ ptr?hτi ≤ pf(F ), Γ ⊢ pf(F ) ≤ int, Γ ⊢ pf(F ) ≤ array(τ),
Γ ⊢ pf(F ) ≤ ptr?hτi, and Γ ⊢ pf(F ) ≤ S(x).
Proof: The proof is by induction on the derivation of the subtyping judgement. The result is clear for all the rules except the transitivity rule.
There is some intermediate type σ that is a supertype of the left type we are considering and a subtype of the right type we are considering.
For the first item, σ is a subtype of S(x) so by the induction hypothesis, σ = S(z) for some z. Since σ is a supertype of τ, by the induction
hypothesis, τ = S(y) for some y, as required. For the second item, σ is a supertype of S(x), so by the induction hypothesis eitehr σ = S(x)
or Γ ⊢ Γ(x) ≤ σ. In the first case, the result follows by the induction hypothesis on the other judgement. In the second case, the result
follows by transitivity of subtyping. For the third item, since σ is a supertype of an array type, by the induction hypothesis, σ must be an
array type, say array(σ′
). Then by the induction hypothesis for both judgements, Γ ⊢ τ1 ≤ σ′
and Γ ⊢ σ′
≤ τ2. By transitivity of
subtyping, Γ ⊢ τ1 ≤ τ2 as required. The fourth and fifth items are similar (the fifth requires transitivity of implication in the logic). For the
sixth item, consider the cases. If the left type is int, an array type, a pointer type, or a proof type, then by the induction hypothesis σ must be
of the same form, so by the induction hypothesis again, the right type must have the same form. These are all the cases we need to consider
for the sixth item.
LEMMA 4 (Canonical Forms). If Γ ⊢P L : Γ@V , x ∈ V , and inscopeP (y) ⊆ V for y ∈ V then:
• If Γ ⊢ S(x) ≤ S(x′
) and x′
∈ V then L(x) = L(x′
).
• If Γ ⊢ Γ(x) ≤ int then L(x) is an integer.
• If Γ ⊢ Γ(x) ≤ array(τ) then L(x) has the form hvi and Γ ⊢P v : τ in L at defP (x).
• If Γ ⊢ Γ(x) ≤ ptr?hτi then L(x) has the form hvi@i and Γ ⊢P v : τ in L at defP (x).
• If Γ ⊢ Γ(x) ≤ pf(F) then L ⊢P F at defP (x).
Proof: For the first item, if the hypothesis holds then by Subtype Inversion either S(x) = S(x′
) or Γ ⊢ Γ(x) ≤ S(x′
). For the former, x = x′
and the conclusion therefore holds. Thus we need only show the first item for the stronger hypothesis that Γ ⊢ Γ(x) ≤ S(x′
) and x′
∈ V .
The proof is by induction on the depth of x in the dominance tree. Since x ∈ V , by the typing rule, Γ ⊢P L(x) : Γ(x) in L at defP (x).
This judgement can be derived by a non-subsumption rule followed by zero or more uses of the subsumption rule. Since subtyping is reflexive
and transitive, the zero or multiple uses of subsumption can be transformed into exactly one use. Consider the non-subsumption rule used:
Singleton Value Rule: In this case, L(x) = L(x′′
), x′′
∈ dom(L), x′′
∈ inscopeP (defP (x)), and Γ ⊢ S(x′′
) ≤ Γ(x). Since
x′′
∈ inscopeP (defP (x)), x′′
∈ V and x′′
is less deep in the dominance tree than x. By the induction hypothesis, the result holds
for x = x′′
, we just need to show that it holds for x. If the hypothesis of the first item holds (Γ ⊢ Γ(x) ≤ S(x′
) and x′
∈ V ),
then by transitivity Γ ⊢ S(x′′
) ≤ S(x′
), so by the induction hypothesis, L(x′′
) = L(x′
). Thus L(x) = L(x′
) as required. If the
hypothesis of the third item holds (Γ ⊢ Γ(x) ≤ array(τ)), then by Subtyping Inversion on Γ ⊢ S(x′′
) ≤ Γ(x) either S(x′′
) = Γ(x) or
Γ ⊢ Γ(x′′
) ≤ Γ(x). For the former, we have Γ ⊢ S(x′′
) ≤ array(τ), so by Subtyping Inversion Γ ⊢ Γ(x′′
) ≤ array(τ). For the latter,
the last judgement holds by transitivity. Then by the induction hypothesis L(x′′
) has the form hvi and Γ ⊢P v : τ in L at defP (x′′
). By
Lemma 1, Γ ⊢P v : τ in L at defP (x), as required. The cases for the second and fourth items are similar to the case for the third item.
If the hypothesis for the fifth item holds then by similar reasoning to the third item, Γ ⊢ Γ(x′′
) ≤ pf(F). By the induction hypothesis,
L ⊢P F at defP (x′′
). By Lemma 1, L ⊢P F at defP (x), as required.
Integer Rule: In this case, L(x) = i for some i and Γ ⊢ int ≤ Γ(x). The second item clearly holds. If the hypothesis of the other items
held then by transitivity of subtyping, int would be a subtype of a singleton, array, pointer, or proof type, which is not possible by
Subtyping Inversion.
Array Rule: In this case, L(x) = hvi, Γ ⊢P v : τ in L at defP (x), and Γ ⊢ array(τ) ≤ Γ(x). If the hypothesis of the third item,
namely Γ ⊢ Γ(x) ≤ array(σ), holds then by transitivity of subtyping and Subtyping Inversion, Γ ⊢ τ ≤ σ. Then by subsumption
Γ ⊢P v : σ in L at defP (x) as required by the conclusion of item three. If the hypothesis of the other items held then by transitivity of
subtyping, array(τ) would be a subtype of a singleton, integer, pointer, or proof type, which is not possible by Subtyping Inversion.
Pointer Rule: In this case, L(x) = hvi@i, Γ ⊢P v : τ in L at defP (x), and Γ ⊢ ptr?hτi ≤ Γ(x). If the hypothesis of the fourth
item, namely Γ ⊢ Γ(x) ≤ ptr?hσi, holds then by transitivity of subtyping and Subtyping Inversion, Γ ⊢ τ ≤ σ. Then by subsumption
POPL ’06 Submission 14 2005/11/15
Γ ⊢P v : σ in L at defP (x) as required by the conclusion of item four. If the hypothesis of the other items held then by transitivity of
subtyping, ptr?hτi would be a subtype of a singleton, integer, array, or proof type, which is not possible by Subtyping Inversion.
Proof Rule: In this case, L ⊢P F′
at defP (x) and Γ ⊢ pf(F′
) ≤ Γ(x). If the hypothesis of the fifth item, name Γ ⊢ Γ(x) ≤ pf(F ), held,
then by transitivity of subtyping and Subtyping Inversion, ⊢ F′
=⇒ F. Then by Logical Soundness, L ⊢P F at defP (x), as required
by the conclusion to item five. If the hypothesis of the other items held then by transitivity of subtyping, pf(F ′) would be a subtype of a
singleton, integer, array, or pointer type, which is not possible by Subtyping Inversion.
LEMMA 5. For any b a block number for P and n an incoming edge number to b, inscopeP (b.i) ⊆ dfndAtP (b.i, n).
Proof: If i > 0 then the result holds by definition. Otherwise let (b′
, b) be the n-th incoming edge to b. Then dfndAtP (b.i, n) =
inscopeP ((b′
, b).1). Let N be the parent of b in the dominance tree for P. If N is not equal to or an ancestor of (b′
, b) then there exists a
path from (−1, 0) to (b′
, b) that does not include N. We can extend this path with the edge from (b′
, b) to b to obtain a path from (−1, 0) to b
that does not include N contradicting the fact that N is b’s parent in the dominance tree. Let x ∈ inscopeP (b.0) then sdomP (defP (x), b.0).
Let defP (x) = b′′
.i then domP (b′′
, N), so domP (b′′
, (b′
, b)). Since (b′
, b).1 does not define any variables, sdomP (defP (x), (b′
, b).1) and
x ∈ inscopeP ((b′
, b).1), as required.
LEMMA 6 (Canonical Forms 2). If Γ ⊢P L : Γ@inscopeP (du) and Γ ⊢P v : τ in L at du then:
• If τ = int then v = i.
• If τ = array(σ) then v = hvi and Γ ⊢P v : σ in L at du.
• If τ = ptr?hσi then v = hvi@i and Γ ⊢P v : σ in L at du.
• If τ = S(x) then v = L(x) and x ∈ inscopeP (du).
• If τ = pf(F ) then v = true and L ⊢P F at du.
Proof: The proof is by induction on the depth of du is the dominance tree. The judgement Γ ⊢P v : τ in L at du can only be derived by
a nonsubsumption rule following by zero or more uses of the subsumption rule. Since subtyping is reflexive and transitive, we can turn these
uses of subsumption into exactly one use. Consider the nonsubsumption rule used:
Singleton Rule: In this case v = L(x), x ∈ inscopeP (du), and Γ ⊢ S(x) ≤ τ. If τ = S(x) then the result holds. Otherwise, by Subtyping
Inversion Γ ⊢ Γ(x) ≤ τ. By hypothesis, Γ ⊢P v : Γ(x) in L at defP (x). By subsumption, Γ ⊢P v : τ in L at defP (x). Since
x ∈ inscopeP (du), defP (x) ∈ inscopeP (du), so sdomP (defP (x), du). Thus defP (x) is higher in the dominance tree than du. The
result follows by the induction hypothesis.
Integer Rule: In this case v is some i and by Subtyping Inversion τ must be int, as required.
Array Rule: In this case v is hvi, Γ ⊢P v : τ1 in L at du, and Γ ⊢ array(τ1) ≤ τ. By Subtyping Inversion τ must be array(τ2) and
Γ ⊢ τ1 ≤ τ2. By subsumption, Γ ⊢P v : τ2 in L at du, as required.
Pointer Rule: In this case v is ARRAY v@i, Γ ⊢P v : τ1 in L at du, and Γ ⊢ ptr?hτ1i ≤ τ. By Subtyping Inversion τ must be ptr?hτ2i
and Γ ⊢ τ1 ≤ τ2. By subsumption, Γ ⊢P v : τ2 in L at du, as required.
Proof Rule: In this case v is true, L ⊢P F1 at du, and Γ ⊢ pf(F1) ≤ τ. By Subtyping Inversion τ must be pf(F2) and ⊢ F1 =⇒ F2. By
Logical Soundness L ⊢P F2 at du, as required.
LEMMA 7. If Γ ⊢P L : Γ@inscopeP (du) then:
• If ⊢P τ at defP (x) and Γ ⊢P v : τ{x1 := x2} in L at du then Γ ⊢P v : τ in L{x1 := L(x2)} at defP (x).
• If ⊢P F at defP (x) and L ⊢P F{x1 := x2} at du then L{x1 := L(x2)} ⊢P F at defP (x).
• If ⊢P e at defP (x) and L ⊢P e{x1 := x2} is v at du then L{x1 := L(x2)} ⊢P e is v at defP (x)
Proof: Let ρ = x1 := x2 and L′
= L{x1 := L(x2)}. The proof is by induction of the structure of τ, F, or e. Consider the different forms
that τ, F, or e could take:
τ = int: In this case, τ = τ{ρ}, so the hypothesis and Canonical Forms 2 imply that v = i. The conclusion then follows by the integer
rule.
τ = array(σ): In this case, τ{ρ} = array(σ{ρ}), so by hypothesis and Canonical Forms 2, v = hvi and Γ ⊢P v : σ{ρ} in L at du, by
the induction hypothesis, Γ ⊢P v : σ in L′
at defP (x), so by the array rule the conclusion holds.
τ = ptr?hσi: In this case, τ{ρ} = ptr?hσ{ρ}i, so by hypothesis and Canonical Forms 2, v = hvi@i and Γ ⊢P v : σ{ρ} in L at du. By
the induction hypothesis, Γ ⊢P v : σ in L′
at defP (x), so by the pointer rule the conclusion holds.
τ = S(z): Let y be ρ(z). Then τ{ρ} = S(y) and by hypothesis and Canonical Forms 2, v = L(y). Since ⊢P τ at defP (x),
z ∈ inscopeP (defP (x)). Clearly L′
(z) = L(y) and z ∈ dom(L′
). Thus by the singleton vlaue rule, Γ ⊢P v : S(z) in L′
at defP (x),
as required.
τ = pf(F ): In this case, τ{ρ} = pf(F {ρ}), so by hypothesis and Canonical Forms 2, v = true and L ⊢P F{ρ} at du. By the induction
hypothesis, L′
⊢P F at defP (x), and the conclusion holds by the proof value rule.
F = e1 rop e2: In this case, F{ρ} = e1{ρ} rop e2{ρ}. Since the hypothesis can be derived by only two rules, it must be the case that
L ⊢P e1{ρ} is v1 at du, L ⊢P e2{ρ} is v2 at du, v1 and v2 have the forms i1 and i2 or the forms hvi@i1 and hvi@i2, and i1 rop i2.
POPL ’06 Submission 15 2005/11/15
By the induction hypothesis, L′
⊢P e1 is v1 at defP (x) and L′
⊢P e2 is v2 at defP (x). The conclusion follows by applying the same
rule.
F = F1 ∧ F2: In this case, F{ρ} = F1{ρ} ∧ F2{ρ}. Since the hypothesis can be derived in only one way, L ⊢P F1{ρ} at du and
L ⊢P F2{ρ} at du. By the induction hypothesis, L′
⊢P F1 at defP (x) and L′
⊢P F2 at defP (x). The conclusion follows the the and
rule.
e = i: In this case the conclusion follows by the integer rule.
e = z: Let y be ρ(z). Then e{ρ} = y. The hypothesis can be derived in only one way, so v = L(y). Clearly, L′
(z) = L(y) and
z ∈ dom(L′
). Since ⊢P e at defP (x), z ∈ inscopeP (defP (x)). Thus by the expression variable rule, L′
⊢P z is v at defP (x),
as required.
e = len(z): Let y be ρ(z). Then e{ρ} = len(y). The hypothesis can be derived in only one way, so L ⊢P y is hv0, . . . , vn−1i at du and
v = n. By the induction hypothesis, L′
⊢P z is hv0, . . . , vn−1i at defP (x). The conclusion follows by the length rule.
e = e1 bop e2: In this case, e{ρ} = e1{ρ} bop e2{ρ}. Since the hypothesis can be derived in only one way, L ⊢P e1{ρ} is i1 at du,
L ⊢P e2{ρ} is i2 at du, and v = i1 bop i2. By the induction hypothesis, L′
⊢P e1 is i1 at defP (x) and L′
⊢P e2 is i2 at defP (x).
The conclusion follows by the binary operation rule.
e = z@e′
: Let y be ρ(z). Then e{ρ} = y@e′
{ρ}. The hypothesis can be derived in only one way, so L ⊢P y is hvi at du,
L ⊢P e′
{ρ} is i at du, and v = hvi@i. By the induction hypothesis, L′
⊢P x is hvi at defP (x) and L′
⊢P e′
is i at defP (x). The
conclusion follows by the pointer rule.
A.3 Preservation
LEMMA 8. If ⊢ P then ⊢ (P, ∅, 0, 0.0).
Proof: Straightforward given that dfndAtP (0.0, 0) = ∅.
LEMMA 9 (Preservation). If ⊢ S1 and S1 7→ S2 then ⊢ S2.
Proof: Assume that ⊢ (P, L1, e1, b.i) and (P, L1, e1, b.i) 7→ (P, L2, e2, pc). Let Γ = vt(P).
By the typing rule for programs:
1. ⊢ P
2. Γ ⊢P L1 : Γ@dfndAtP (b.i, e1)
3. e1 is a valid in-edge number for b
4. b.i ∈ pcs(P)
5. ∀x ∈ dfndAtP (b.i, e1) if deffactP (x) = F then L1 ⊢P F at b.i
If P(b.i) = ι or P(b.i) = p then pc = b.(i + 1) ∈ pcs(P) and e2 = e1 so e2 is a valid in-edge number for pc’s block. We will show that
validity of pc and e2 for transfers in the respective rules below. Thus it remains to show that:
1. Γ ⊢P L2 : Γ@dfndAtP (pc, e2)
2. ∀x ∈ dfndAtP (pc, e2) if deffactP (x) = F then L2 ⊢P F at pc
For all instructions for which deffactP (x) is not defined, note that (2) follows immediately by lemma 1, since the set of defined facts remains
unchanged. For instructions for which deffactP (x) = F, it suffices to show that L2 ⊢P F at pc.
The proof proceeds by case analysis on the reduction rule.
Phi rule: In this case, P(b.i) = p, p[e1] = x1 := x2, and L2 = L1{x1 := L1(x2)}. By the definitions, dfndAtP (pc, e2) =
inscopeP (pc) = inscopeP (b.i) ∪ {x1}. By Lemma 5, inscopeP (b.i) ⊆ dfndAtP (b.i, e1). Clearly by the typing rules and Γ ⊢P
L1 : Γ@dfndAtP (b.i, e1), Γ ⊢P L1 : Γ@inscopeP (b.i). So by Lemma 2, we just need to show that Γ ⊢P L1(x2) : Γ(x1) in L2 at
defP (x1) (note that φ instructions define no facts).
Let (b′
, b) be the e1’th incoming edge to b. Since the phi instructions are uses of x2 at (b′
, b).1, defP (x2) ⊆ inscopeP ((b′
, b).1) =
dfndAtP (b.i, e1). By Env Typing, x2 ⊆ dom(L1). Thus by the singleton typing rule, Γ ⊢P L1(x2) : S(x2) in L1 at (b′
, b).1. By
the typing rules for phi-instructions, Γ ⊢ S(x2) ≤ Γ(x1){ρ} where ρ is x1 := x2. Thus by subsumption, Γ ⊢P L1(x2) : Γ(x1){ρ} in
L1 at (b′
, b).1. By the typing rules, ⊢P Γ(x1) at defP (x1). So by Lemma 7, Γ ⊢P L1(x2) : Γ(x1) in L2 at defP (x1).
Constant rule: In this case, P(b.i) = x := i and L2 = L1{x := i}. Also note that deffactP (x) = (x = i).
By expansion of the definitions:
defP (x) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x}
x /
∈ dfndAtP (b.i, e1)
First we must show that:
L2 ⊢P x = i at pc
By the environment rule (since L2(x) = i):
L2 ⊢P x is i at pc
By the integer rule:
L2 ⊢P i is i at pc
So by the comparison rule:
L2 ⊢P x = i at pc
POPL ’06 Submission 16 2005/11/15
By Lemma 2, it suffices to show that Γ ⊢P i : Γ(x) in L2 at b.i.
By assumption:
Γ ⊢P x : τ := i
So by inversion:
Γ ⊢ int ≤ Γ(x)
So by construction using the integer rule and subsumption:
Γ ⊢P i : Γ(x) in L2 at b.i
Copy rule: In this case, P(b.i) = x1 := x2 and L2 = L1{x1 := L1(x2)}. By expansion of the definitions, defP (x1) = b.i,
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1}, and x1 /
∈ dfndAtP (b.i, e1). By Lemma 2, we need to show that Γ ⊢P L1(x2) :
Γ(x1) in L2 at b.i. The typing rule for this instruction include Γ ⊢ S(x2) ≤ Γ(x1). Since this instruction is a use of x2 and the
in-scope property, x2 ∈ dfndAtP (b.i, e1), thus x1 6= x2, x2 ∈ dom(L1), L2(x2) = L1(x2), and x2 ∈ inscopeP (b.i). By the singleton
typing rule and subsumption, Γ ⊢P L1(x2) : Γ(x1) in L2 at b.i, as required.
New array (i ≥ 0) In this case P(b.i) = x1 : τ := newarray(x2, x3) and L2 = L1{x1 := v1}, where L1(x2) = n, L1(x3) = v3, v1 =
hv3, . . . , v3
| {z }
n
i.
By expansion of the definitions:
defP (x1) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x}
x2, x3 ∈ dfndAtP (b.i, e1)
x1 /
∈ dfndAtP (b.i, e1)
By Lemma 2, it suffices to show that Γ ⊢P v1 : Γ(x1) in L2 at b.i.
By assumption:
Γ ⊢P x1 : τ := newarray(x2, x3)
By inversion of Γ ⊢P x1 : τ := newarray(x2, x3):
Γ ⊢ array(Γ(x3)) ≤ Γ(x1)
By assumption (since x3 ∈ dfndAtP (b.i, e1)):
Γ ⊢P v3 : Γ(x3) in L2 at b.i
So by construction, using the newarray rule and subsumption:
Γ ⊢P v1 : Γ(x1) in L2 at b.i
New array (i < 0)
The proof proceeds exactly as in the previous case, except that there is no proof obligation for v3, and hence the construction from the
newarray rule follows immediately.
Array length rule In this case, P(b.i) = x1 : τ := len(x2) and L2 = L1{x1 := n} where L1(x2) = hv0, . . . , vn−1i. Also note that
deffactP (x1) = (x = len(x2))
By expansion of the definitions:
defP (x1) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x}
x2 ∈ dfndAtP (b.i, e1)
x1 /
∈ dfndAtP (b.i, e1)
First we must show that:
L2 ⊢P x1 = len(x2) at pc
By the environment rule (since L2(x1) = n, and L2(x2) = hv0, . . . , vn−1i):
L2 ⊢P x1 is n at pc
L2 ⊢P x2 is hv0, . . . , vn−1i at pc
So by the length rule:
L2 ⊢P len(x2) is n at pc
So by the comparison rule:
L2 ⊢P x1 = len(x2) at pc
By Lemma 2, it suffices to show that Γ ⊢P n : Γ(x1) in L2 at b.i.
By assumption:
Γ ⊢P x1 : τ := len(x2)
By inversion:
Γ ⊢ int ≤ Γ(x1)
So the result holds by construction using the integer rule and subsumption.
Pointer base rule In this case, P(b.i) = x1 : τ := base(x2) and L2 = L1{x2 := v@0}, where L1(x2) = v, v = hv′i. Note that
deffactP (x1) = (x1 = x2@0)
By expansion of the definitions:
defP (x1) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1}
x2 ∈ dfndAtP (b.i, e1)
x1 /
∈ dfndAtP (b.i, e1)
POPL ’06 Submission 17 2005/11/15
First we must show that:
L2 ⊢P x1 = x2@0 at pc
By the environment rule (since L2(x1) = v@0, and L2(x2) = v):
L2 ⊢P x1 is v@0 at pc
L2 ⊢P x2 is v at pc
So by the managed pointer rule:
L2 ⊢P x2@0 is v@0 at pc
So by the comparison rule:
L2 ⊢P x1 = x2@0 at pc
By Lemma 2, it suffices to show that Γ ⊢P v@0 : Γ(x1) in L2 at b.i.
By assumption:
Γ ⊢P x1 : τ := base(x2)
By inversion:
Γ ⊢ Γ(x2) ≤ array(τ2)
Γ ⊢ ptr?hτ2i ≤ Γ(x1)
So by Canonical Forms:
Γ ⊢P v′ : τ2 in L1 at defP (x2)
Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and x1 /
∈ inscopeP (defP (x1)).
So by lemma 1:
Γ ⊢P v′ : τ2 in L2 at b.i
So the result holds by construction using the managed pointer rule and subsumption.
Binary op rule (int) In this case, P(b.i) = x1 : τ := x2 bop x3 and L2 = L1{x1 := i2 bop i3}: where L1(x2) = i2, L1(x3) = i3. Note
that deffactP (x1) = (x1 = x2 bop x3).
By expansion of the definitions:
defP (x1) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1}
x2, x3 ∈ dfndAtP (b.i, e1)
x1 /
∈ dfndAtP (b.i, e1)
First we must show that:
L2 ⊢P x1 = x2 bop x3 at pc
By the environment rule (since L2(x1) = i2 bop i3, L2(x2) = i2, and L2(x3) = i3):
L2 ⊢P x1 is i2 bop i3 at pc
L2 ⊢P x2 is i2 at pc
L2 ⊢P x3 is i3 at pc
So by the integer arithmetic rule:
L2 ⊢P x2 bop x3 is i2 bop i3 at pc
So by the comparison rule:
L2 ⊢P x1 = x2 bop x3 at pc
By Lemma 2, it suffices to show that Γ ⊢P i2 bop i3 : Γ(x1) in L2 at b.i.
By assumption:
Γ ⊢P x1 : τ := x2 bop x3
So by inversion:
Γ ⊢ int ≤ Γ(x1)
So the result holds by construction using the integer rule and subsumption.
Binary op rule (pointer) In this case, P(b.i) = x1 : τ := x2 bop x3 and L2 = L1{x1 := v@i2 bop i3}: where L1(x2) =
v@i2, L1(x3) = i3. Note that deffactP (x1) = (x1 = x2 bop x3).
By expansion of the definitions:
defP (x1) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1}
x2, x3 ∈ dfndAtP (b.i, e1)
x1 /
∈ dfndAtP (b.i, e1)
First we must show that:
L2 ⊢P x1 = x2 bop x3 at pc
By the environment rule (since L2(x1) = v@(i2 bop i3), L2(x2) = v@i2, and L2(x3) = i3):
L2 ⊢P x1 is v@(i2 bop i3) at pc
L2 ⊢P x2 is v@i2 at pc
L2 ⊢P x3 is i3 at pc
So by the pointer arithmetic rule:
L2 ⊢P x2 bop x3 is v@(i2 bop i3) at pc
So by the pointer comparison rule:
L2 ⊢P x1 = x2 bop x3 at pc
By Lemma 2, it suffices to show that Γ ⊢P v@i2 bop i3 : Γ(x1) in L2 at b.i.
POPL ’06 Submission 18 2005/11/15
By assumption:
Γ ⊢P x1 : τ := x2 bop x3
So by inversion:
Γ ⊢ Γ(x2) ≤ ptr?hτ2i
Γ ⊢ Γ(x3) ≤ int
Γ ⊢ ptr?hτ2i ≤ Γ(x1)
So by Canonical Forms:
Γ ⊢P v : τ2 in L1 at defP (x2)
Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and x1 /
∈ inscopeP (defP (x1)).
So by lemma 1:
Γ ⊢P v : τ2 in L2 at b.i
So the result holds by construction using the managed pointer rule and subsumption.
Load rule In this case, P(b.i) = x1 : τ := ld(x2) [x3] and L2 = L1{x1 := vi}: where L1(x2) = hv0, . . . , vni@i, 0 ≤ i ≤ n.
By expansion of the definitions:
defP (x1) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1}
x2, x3 ∈ dfndAtP (b.i, e1)
x1 /
∈ dfndAtP (b.i, e1)
By Lemma 2, it suffices to show that Γ ⊢P vi : Γ(x1) in L2 at b.i.
By assumption:
Γ ⊢P x1 : τ := ld(x2) [x3]
So by inversion:
Γ ⊢ Γ(x2) ≤ ptr?hτ2i
Γ ⊢ Γ(x3) ≤ pf(x@0≤x2∧x2<x@len(x))
Γ ⊢ τ2 ≤ Γ(x1)
So by Canonical Forms:
Γ ⊢P v : τ2 in L1 at defP (x2)
Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and that x1 /
∈ inscopeP (defP (x1)).
So by lemma 1:
Γ ⊢P v : τ2 in L2 at b.i
So in particular:
Γ ⊢P vi : τ2 in L2 at b.i
So the result holds by subsumption.
Proof Fact In this case, P(b.i) = x1 : τ := pffact(x2) and L2 = L1{x1 := true}.
By expansion of the definitions:
defP (x1) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1}
x2 ∈ dfndAtP (b.i, e1)
x1 /
∈ dfndAtP (b.i, e1)
By Lemma 2, it suffices to show that Γ ⊢P true : Γ(x1) in L2 at b.i.
By assumption:
Γ ⊢P x1 : τ := pffact(x2)
By inversion:
Γ ⊢ pf(deffactP (x2)) ≤ Γ(x1)
By assumption, L is consistent. Therefore, since x2 ∈ inscopeP (b.i), L1 ⊢P deffactP (x2) at defP (x2).
Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and x1 /
∈ inscopeP (b.i).
So by lemma 1:
L2 ⊢P deffactP (x2) at b.i
By the true rule:
Γ ⊢P true : pf(deffactP (x2)) in L2 at b.i
By subsumption:
Γ ⊢P true : Γ(x1) in L2 at b.i
Proof conjunction In this case, P(b.i) = x : τ := pfand(y1, . . . , yn) and L2 = L1{x := true}.
By expansion of the definitions:
defP (x) = b.i
dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x}
y1, . . . , yn ∈ dfndAtP (b.i, e1)
x /
∈ dfndAtP (b.i, e1)
By Lemma 2, it suffices to show that Γ ⊢P true : Γ(x) in L2 at b.i.
By assumption:
Γ ⊢P x : τ := pfand(y1, . . . , yn)
POPL ’06 Submission 19 2005/11/15
By inversion:
Γ ⊢ Γ(y1) ≤ pf(F1)
· · ·
Γ ⊢ Γ(yn) ≤ pf(Fn)
Γ ⊢ pf(F1∧···∧Fn) ≤ Γ(x)
By Canonical Forms:
L1 ⊢P F1 at defP (y1)
· · ·
L1 ⊢P Fn at defP (yn)
Note that b.i is a use of x1 through yn, so by the in-scope property, sdomP (defP (y1), b.i) through sdomP (defP (yn), b.i).
So by lemma 1:
L1 ⊢P F1 at b.i
· · ·
L1 ⊢P Fn at b.i
By the conjunction rule:
L1 ⊢P F1 ∧ · · · ∧ Fn at b.i
Note that x /
∈ inscopeP (b.i).
Therefore by Weakening (lemma 1):
L2 ⊢P F1 ∧ · · · ∧ Fn at b.i
By the true intro rule:
Γ ⊢P true : F1 ∧ · · · ∧ Fn in L2 at b.i.
By subsumption:
Γ ⊢P true : Γ(x) in L2 at b.i.
Conditional Branch Rule In this case, P(b.i) = [x1 : τ1, x2 : τ2] if x3 rop x4 goto b′
and L2 = L1{xj := true}: where L1(x3) =
i3, L1(x4) = i4, where j = 1 if ¬(i3 rop i4) and where j = 2 if i3 rop i4.
It suffices to show:
edgeP (b, b′
) is an in-edge number for b′
edgeP (b, b + 1) is an in-edge number for (b + 1)
b′
.0 and (b + 1).0 are in pcs(P)
Γ ⊢P L2 : Γ@dfndAtP (pc, e2)
By the context-sensitive syntactic restrictions on programs, b′
must be a block number in the program, and b must not be the last block
in the program. Therefore, by definition, b′
.0 and (b + 1).0 are in pcs(P). Also by definition, there are edges in the program (b, b′
) and
(b, b + 1): so the in-edge numbers are likewise well-defined.
It remains to show that Γ ⊢P L2 : Γ@dfndAtP (pc, e2). There are two cases: ¬(i3 rop i4) and L1{x1 := true}, or i3 rop i4 and
L1{x2 := true}.
Suppose ¬(i3 rop i4).
By Lemma 2, it suffices to show that Γ ⊢P true : Γ(x1) in L2 at b.i.
Since L1(x3) = i3 and L1(x4) = i4, by the environment rule:
L1 ⊢P x3 is i3 at b.i
L1 ⊢P x4 is i4 at b.i
By assumption, ¬(i3 rop i4), so by the comparison rule:
L1 ⊢P pf(¬(x3 rop x4)) at b.i
So by lemma 1:
L2 ⊢P pf(¬(x3 rop x4)) at b.i
So by the true introduction rule:
Γ ⊢P true : pf(¬(x3 rop x4)) in L2 at b.i
By inversion of the typing derivation for the instruction:
Γ ⊢ pf(¬(x3 rop x4)) ≤ Γ(x1)
Γ ⊢ pf(x3 rop x4) ≤ Γ(x2)
So by subsumption:
Γ ⊢P true : Γ(x1) in L2 at b.i
The argument is similar when i3 rop i4).
Goto rule: In this case P(b.i) = goto b′
, L2 = L1, pc = b′
.0, and e2 = edgeP (b, b′
). By the syntactic restrictions b′
must be a
valid block number, so b′
.0 ∈ pcs(P). Since (b, b′
) is an edge, edgeP (b, b′
) is a valid in-edge number for b′
. By the definitions
dfndAtP (pc, e2) = inscopeP ((b, b′
).1) = inscopeP (b.i) = dfndAtP (b.i, e1). Thus Γ ⊢P L2 : Γ@dfndAtP (pc, e2) follows from
(2).
A.4 Progress
LEMMA 10 (Env Typing). If Γ ⊢P L : Γ@dfndAtP (pc, n) and x ∈ dfndAtP (pc, n), then L(x) is well defined.
Proof: By inversion of the environment typing rules.
LEMMA 11 (Progress). If ⊢ S then S is not stuck.
POPL ’06 Submission 20 2005/11/15
Proof: Assume that ⊢ S and S = (P, L, e, b.i).
Recall that by the definition of ⊢ S
⊢ P vt(P) = Γ
Γ ⊢P L : Γ@dfndAtP (pc, n)
n is an in-edge number for b where pc = b.i
pc ∈ pcs(P)
And by the definition of ⊢ P
P satisfies the SSA property
For each x ∈ vt(P), and each y ∈ fv(vt(P)), sdomP (defP (y), defP (x))
vt(P) ⊢ p for every p in P
vt(P) ⊢P ι for every instruction ι in P
vt(P) ⊢ c for every transfer c in P
The proof proceeds by case analysis on P(b.i).
p:
Let x1 := x2 = p[e] and (b′
, b) be the e’th incoming edge to b (this is well defined by the type rules).
By the use/def definition, the instruction is a use of x2 at (b′
, b).1, so by the in-scope property x2 ∈ dfndAtP (b.i, e) (since i = 0).
By the definition of ⊢ S above and by lemma 10, note that x2 ∈ dom(L) and hence L(x2) are well defined.
Therefore S 7→ (P, L{x1 := L(x2)}, e, b.(i + 1)).
x : τ := i:
In this case, S 7→ (P, L{x := i}, e, b.(i + 1)).
x1 : τ := x2:
In this case, since this instruction is a use of x2, by the in-scope property, x2 ∈ inscopeP (b.i).
So by definition, x2 ∈ dfndAtP (b.i, e), and so by lemma 10 L(x2) is defined.
Therefore S 7→ (P, L{x1 := L(x2)}, e, b.(i + 1)).
x1 : τ := newarray(x2, x3):
It suffices to show that L(x2) = n for some integer n, and (in the case that n >= 0) that L(x3) = v3 for some value v3.
By definition, x2, x3 ∈ inscopeP (b.i), and so by definition, x2.x3 ∈ dfndAtP (b.i, e).
Therefore, by 10 L(x2) = v2 and L(x3) = v3 for some v2, v3. It suffices to show that v2 = n for some integer n.
By assumption, Γ ⊢ Γ(x2) ≤ int, and Γ ⊢P L : Γ@dfndAtP (b.i, e), so by Canonical Forms (lemma 4), L(x2) = v2 = n for some
integer n.
x1 : τ := len(x2):
It suffices to show that L(x2) = hv0, . . . , vn−1i.
By assumption, Γ ⊢ Γ(x2) ≤ array(τ2) and Γ ⊢P L : Γ@dfndAtP (b.i, e), so by Canonical Forms (lemma 4) L(x2) = hv0, . . . , vn−1i
for some n.
x1 : τ := base(x2):
It suffices to show that L(x2) = v, v = hv′i for some v, v′
.
By assumption, Γ ⊢ Γ(x2) ≤ array(τ2) and Γ ⊢P L : Γ@dfndAtP (b.i, e), so by Canonical Forms (lemma 4) L(x2) = hv0, . . . , vn−1i
for some n.
x1 : τ := x2 bop x3:
It suffices to show that L(x2) = v2, L(x3) = i3, for some integer i3, and where either v2 = i2 or v2 = v@i2 for some integer i2 and
value v.
Recall that by assumption, Γ ⊢P L : Γ@dfndAtP (b.i, e), and by the inscope property, x2, x3 ∈ dfndAtP (b.i, e).
By assumption, Γ ⊢ Γ(x3) ≤ int so by Canonical Forms (lemma 4) L(x3) = i3.
There are two cases to consider for x2, corresponding to the two possible last typing rules of the derivation.
1. Suppose the last rule was the integer operation rule. Then by assumption, Γ ⊢ Γ(x2) ≤ int, and so by Canonical Forms (lemma 4)
L(x2) = i2.
2. Suppose the last rule was the managed pointer operation rule. Then by assumption, Γ ⊢ Γ(x2) ≤ ptr?hτ2i, and so by canonical
forms, L(x2) = v@i2.
x1 : τ := ld(x2) [x3]:
It suffices to show that L(x2) = hv0, . . . , vni@i and that 0 ≤ i ≤ n.
By assumption, Γ ⊢ Γ(x2) ≤ ptr?hτ2i and by the in-scope property, x2 ∈ dfndAtP (b.i, e), so by Canonical Forms (lemma 4),
L(x2) = hv0, . . . , vni@i.
Also by assumption, Γ ⊢ Γ(x3) ≤ pf(x@0≤x2∧x2<x2@len(x)), so again by the in-scope property Canonical Forms applies. Therefore,
L ⊢P (x@0≤x2 ∧ x2<x2@len(x)) at defP (x3), for some x.
Let D be the derivation of L ⊢P (x@0≤x2 ∧ x2<x2@len(x)) at defP (x3). Note that this derivation has a unique last rule.
By inversion of D :
L ⊢P x@0≤x2 at defP (x3)
The derivation of L ⊢P x@0≤x2 at defP (x3) must end in one of the two comparison rules (integer or pointer). Note though that by
Canonical Forms (above) L(x2) = hv0, . . . , vni@i, and therefore the only derivation possible for the second premise of the comparison
rules is that L ⊢P x2 is hv0, . . . , vni@i2 at defP (x3).
POPL ’06 Submission 21 2005/11/15
Therefore, by inversion, we have:
L ⊢P x@0 is hv0, . . . , vni@i1 at defP (x3)
L ⊢P x2 is hv0, . . . , vni@i2 at defP (x3)
i1 ≤ i2
By inverting the first sub-derivation, we have:
L ⊢P x is hv0, . . . , vni at defP (x3)
L ⊢P e is 0 at defP (x3)
Therefore, i1 = 0. By inverting the second sub-derivation, we have L(x2) = hv0, . . . , vni@i2, and by the Canonical Forms
L(x2) = hv0, . . . , vni@i, so by transitivity, we have i = i2. Finally, recall that i1 ≤ i2, so we have 0 ≤ i.
It remains to be shown that i ≤ n.
By inversion of D :
L ⊢P x2<x2@len(x) at defP (x3)
By the same argument as above, this derivation must be a use of the pointer comparison rule.
Therefore, by inversion:
L ⊢P x2 is hv0, . . . , vni@i2 at defP (x3)
L ⊢P x@len(x) is hv0, . . . , vni@ix at defP (x3)
i2 < ix
By the same argument as above, i2 = i. It therefore suffices to show that ix = n + 1.
By inversion of L ⊢P x@len(x) is hv0, . . . , vni@ix at defP (x3):
L ⊢P x is hv0, . . . , vni at defP (x3)
L ⊢P len(x) is ix at defP (x3)
But note that, L(x) = hv0, . . . , vni, so L ⊢P len(x) is n + 1 at defP (x3), and hence ix = n + 1.
x1 : τ := pffact(x2):
The reduction rule for this instruction always applies.
x1 : τ := pfand(x2, x3):
The reduction rule for this instruction always applies.
[x1 : τ1, x2 : τ2] if x3 rop x4 goto b′
:
It suffices to show that:
L1(x3) = i3 for some integer i3
L1(x4) = i4 for some integer i4
edgeP (b, b + 1) is well-defined
edgeP (b, b′
) is well-defined
Note that x3, x4 ∈ inscopeP (b.i), so x3, x4 ∈ dfndAtP (b.i, e).
By assumption:
Γ ⊢ Γ(x3) ≤ int
Γ ⊢ Γ(x4) ≤ int
So by Canonical Forms (lemma 4)
L1(x3) = i3 for some integer i3
L1(x4) = i4 for some integer i4
Finally, by definition, (b, b′
) and (b, b + 1) are in edges(P) and hence edgeP (b, b + 1) and edgeP (b, b′
) are well-defined.
goto b′
:
It suffices to show that edgeP (b, b′
) is well-defined, which follows immediately since by definition, (b, b′
) is in edges(P).
A.5 Type Safety
Proof: [of Type Safety] The proof is by induction, Lemma 8, Preservation, and Progress.
POPL ’06 Submission 22 2005/11/15

More Related Content

PDF
Dynamic Multi Levels Java Code Obfuscation Technique (DMLJCOT)
PDF
A Platform for Application Risk Intelligence
PDF
Developing Safety-Critical Java Applications with oSCJ
PDF
Ijetcas14 385
DOCX
HR OPERATION MANAGER
PDF
All experiment of java
PDF
SOURCE CODE ANALYSIS TO REMOVE SECURITY VULNERABILITIES IN JAVA SOCKET PROGR...
PDF
WIRELESS COMPUTING AND IT ECOSYSTEMS
Dynamic Multi Levels Java Code Obfuscation Technique (DMLJCOT)
A Platform for Application Risk Intelligence
Developing Safety-Critical Java Applications with oSCJ
Ijetcas14 385
HR OPERATION MANAGER
All experiment of java
SOURCE CODE ANALYSIS TO REMOVE SECURITY VULNERABILITIES IN JAVA SOCKET PROGR...
WIRELESS COMPUTING AND IT ECOSYSTEMS

Similar to A Verifiable SSA Program Representation For Aggressive Compiler Optimization (20)

PDF
Java: A Secure Programming Language for Today's Market
PPTX
SOHIL_RM (1).pptx
PDF
BEST FINAL YEAR PROJECT IEEE 2015 BY SPECTRUM SOLUTIONS PONDICHERRY
PDF
Online java compiler with security editor
PDF
SOURCE CODE ANALYSIS TO REMOVE SECURITY VULNERABILITIES IN JAVA SOCKET PROGRA...
PDF
Software Reverse Engineering in a Security Context
PDF
DEEP LEARNING SOLUTIONS FOR SOURCE CODE VULNERABILITY DETECTION
PDF
20100309 03 - Vulnerability analysis (McCabe)
PDF
Automated server-side model for recognition of security vulnerabilities in sc...
PDF
A Resiliency Framework For An Enterprise Cloud
PPTX
OWASP_Top_Ten_Proactive_Controls_v2.pptx
PDF
Security in Java
PPTX
OWASP_Top_Ten_Proactive_Controls_v2.pptx
PPTX
OWASP_Top_Ten_Proactive_Controls_v2.pptx
PPTX
OWASP_Top_Ten_Proactive_Controls version 2
DOCX
Defensive coding practices is one of the most critical proactive s
PDF
Developing microservices with Java and applying Spring security framework and...
PDF
Binary code obfuscation through c++ template meta programming
DOCX
Securing class initialization in java like languages
DOCX
JAVA 2013 IEEE NETWORKSECURITY PROJECT Securing class initialization in java ...
Java: A Secure Programming Language for Today's Market
SOHIL_RM (1).pptx
BEST FINAL YEAR PROJECT IEEE 2015 BY SPECTRUM SOLUTIONS PONDICHERRY
Online java compiler with security editor
SOURCE CODE ANALYSIS TO REMOVE SECURITY VULNERABILITIES IN JAVA SOCKET PROGRA...
Software Reverse Engineering in a Security Context
DEEP LEARNING SOLUTIONS FOR SOURCE CODE VULNERABILITY DETECTION
20100309 03 - Vulnerability analysis (McCabe)
Automated server-side model for recognition of security vulnerabilities in sc...
A Resiliency Framework For An Enterprise Cloud
OWASP_Top_Ten_Proactive_Controls_v2.pptx
Security in Java
OWASP_Top_Ten_Proactive_Controls_v2.pptx
OWASP_Top_Ten_Proactive_Controls_v2.pptx
OWASP_Top_Ten_Proactive_Controls version 2
Defensive coding practices is one of the most critical proactive s
Developing microservices with Java and applying Spring security framework and...
Binary code obfuscation through c++ template meta programming
Securing class initialization in java like languages
JAVA 2013 IEEE NETWORKSECURITY PROJECT Securing class initialization in java ...

More from Joe Osborn (20)

PDF
Research Paper Template Outline For A Short Resear
PDF
Narrative Essay Graphic Organizer BrainPOP Edu
PDF
Fantastic College Admissions Essay Help
PDF
College Essay Career Goals Career Goals Essay Exa
PDF
Professional Paper Writing
PDF
Consumer Reports Buying Guide 2022 Mustang
PDF
College Athletes Should Get Paid Argument Essay. Should College
PDF
Edit My College Essay. College Essay Editing And Proofreading Service
PDF
Academic Writing CELC E-Resources
PDF
Patriotic Computer Paper - TCR5894 Teacher Create
PDF
Quality Writing Paper. Paper Help For Studen
PDF
Scholarship Application Essay Format. Write A Tel
PDF
An Effective Topic Sentence Will Reflect Which Of The Following
PDF
General Agreement Contract Template Word 2003 Mas
PDF
21 Hilarious Tumblr Posts To Read When
PDF
Film Analysis Paper. How To Write A Film Analysis Essa
PDF
White Writing On Black Background Cotton F
PDF
A Feminist Critical Discourse Analysis Of Qaisra Shahraz S The Holy Woman In ...
PDF
A Glance To Teachers Work With Resources Case Of Olcay
PDF
3G RBS Overview Field Support Operations Contents
Research Paper Template Outline For A Short Resear
Narrative Essay Graphic Organizer BrainPOP Edu
Fantastic College Admissions Essay Help
College Essay Career Goals Career Goals Essay Exa
Professional Paper Writing
Consumer Reports Buying Guide 2022 Mustang
College Athletes Should Get Paid Argument Essay. Should College
Edit My College Essay. College Essay Editing And Proofreading Service
Academic Writing CELC E-Resources
Patriotic Computer Paper - TCR5894 Teacher Create
Quality Writing Paper. Paper Help For Studen
Scholarship Application Essay Format. Write A Tel
An Effective Topic Sentence Will Reflect Which Of The Following
General Agreement Contract Template Word 2003 Mas
21 Hilarious Tumblr Posts To Read When
Film Analysis Paper. How To Write A Film Analysis Essa
White Writing On Black Background Cotton F
A Feminist Critical Discourse Analysis Of Qaisra Shahraz S The Holy Woman In ...
A Glance To Teachers Work With Resources Case Of Olcay
3G RBS Overview Field Support Operations Contents

Recently uploaded (20)

PDF
IGGE1 Understanding the Self1234567891011
PDF
AI-driven educational solutions for real-life interventions in the Philippine...
PDF
OBE - B.A.(HON'S) IN INTERIOR ARCHITECTURE -Ar.MOHIUDDIN.pdf
PPTX
A powerpoint presentation on the Revised K-10 Science Shaping Paper
PDF
CISA (Certified Information Systems Auditor) Domain-Wise Summary.pdf
PDF
MBA _Common_ 2nd year Syllabus _2021-22_.pdf
PDF
1.3 FINAL REVISED K-10 PE and Health CG 2023 Grades 4-10 (1).pdf
PPTX
History, Philosophy and sociology of education (1).pptx
PPTX
TNA_Presentation-1-Final(SAVE)) (1).pptx
PDF
Chinmaya Tiranga quiz Grand Finale.pdf
PPTX
20th Century Theater, Methods, History.pptx
PPTX
Introduction to pro and eukaryotes and differences.pptx
PPTX
202450812 BayCHI UCSC-SV 20250812 v17.pptx
PDF
Practical Manual AGRO-233 Principles and Practices of Natural Farming
PPTX
CHAPTER IV. MAN AND BIOSPHERE AND ITS TOTALITY.pptx
PDF
Weekly quiz Compilation Jan -July 25.pdf
PDF
Paper A Mock Exam 9_ Attempt review.pdf.
PPTX
Unit 4 Computer Architecture Multicore Processor.pptx
DOC
Soft-furnishing-By-Architect-A.F.M.Mohiuddin-Akhand.doc
PPTX
Onco Emergencies - Spinal cord compression Superior vena cava syndrome Febr...
IGGE1 Understanding the Self1234567891011
AI-driven educational solutions for real-life interventions in the Philippine...
OBE - B.A.(HON'S) IN INTERIOR ARCHITECTURE -Ar.MOHIUDDIN.pdf
A powerpoint presentation on the Revised K-10 Science Shaping Paper
CISA (Certified Information Systems Auditor) Domain-Wise Summary.pdf
MBA _Common_ 2nd year Syllabus _2021-22_.pdf
1.3 FINAL REVISED K-10 PE and Health CG 2023 Grades 4-10 (1).pdf
History, Philosophy and sociology of education (1).pptx
TNA_Presentation-1-Final(SAVE)) (1).pptx
Chinmaya Tiranga quiz Grand Finale.pdf
20th Century Theater, Methods, History.pptx
Introduction to pro and eukaryotes and differences.pptx
202450812 BayCHI UCSC-SV 20250812 v17.pptx
Practical Manual AGRO-233 Principles and Practices of Natural Farming
CHAPTER IV. MAN AND BIOSPHERE AND ITS TOTALITY.pptx
Weekly quiz Compilation Jan -July 25.pdf
Paper A Mock Exam 9_ Attempt review.pdf.
Unit 4 Computer Architecture Multicore Processor.pptx
Soft-furnishing-By-Architect-A.F.M.Mohiuddin-Akhand.doc
Onco Emergencies - Spinal cord compression Superior vena cava syndrome Febr...

A Verifiable SSA Program Representation For Aggressive Compiler Optimization

  • 1. A Verifiable SSA Program Representation for Aggressive Compiler Optimization Vijay S. Menon1 Neal Glew1 Brian R. Murphy2 Andrew McCreight3 ∗ Tatiana Shpeisman1 Ali-Reza Adl-Tabatabai1 Leaf Petersen1 1 Intel Labs 2 Intel China Research Center 3 Dept. of Computer Science, Yale University Santa Clara, CA 95054 Beijing, China New Haven, CT 06520 {vijay.s.menon, brian.r.murphy, tatiana.shpeisman, ali-reza.adl-tabatabai, leaf.petersen}@intel.com aglew@acm.org andrew.mccreight@yale.edu Abstract We present a verifiable low-level program representation to em- bed, propagate, and preserve safety information in high perfor- mance compilers for safe languages such as Java and C#. Our rep- resentation precisely encodes safety information via static single- assignment (SSA) [11, 3] proof variables that are first-class con- structs in the program. We argue that our representation allows a compiler to both (1) express aggressively optimized machine-independent code and (2) leverage existing compiler infrastructure to preserve safety information during optimization. We demonstrate that this ap- proach supports standard compiler optimizations, requires minimal changes to the implementation of those optimizations, and does not artificially impede those optimizations to preserve safety. We also describe a simple type system that formalizes type safety in an SSA-style control-flow graph program representation. Through the types of proof variables, our system enables composi- tional verification of memory safety in optimized code. Finally, we discuss experiences integrating this representation into the machine-independent global optimizer of STARJIT, a high-performance just-in-time compiler that performs aggressive control-flow, data-flow, and algebraic optimizations and is compet- itive with top production systems. Categories and Subject Descriptors D.3.1 [Programming Lan- guages]: Formal Definitions and Theory; D.3.4 [Programming Languages]: Compilers; D.3.4 [Programming Languages]: Opti- mization; F.3.1 [Logics and Meanings of Programs]: Specifying and Verifying and Reasoning about Programs General Terms Performance, Design, Languages, Reliability, Theory, Verification Keywords Typed Intermediate Languages, Proof Variables, Safety Dependences, Check Elimination, SSA Formalization, Type Sys- tems, Typeability Preservation, Intermediate Representations ∗ Supported in part by NSF grants CCR-0208618 and CCR-0524545. [copyright notice will appear here] 1. Introduction In the past decade, safe languages have become prevalent in the general software community and have gained wide acceptance among software developers. Safe languages such as Java and C# are particularly prominent. These languages provide a C++-like syn- tax and feature set in conjunction with verifiable safety properties. Foremost among these properties is memory safety, the guarantee that a program will only read or write valid memory locations. Memory safety is crucial to both robustness and security. It pre- vents common programmer memory errors and security exploits such as buffer overruns through a combination of compile-time and run-time checks. Both Java and C# were designed to allow programs to be com- piled and distributed via bytecode formats. These formats retain the crucial safety properties of the source language and are themselves statically verifiable. Managed runtime environments (MRTEs), such as the Java Virtual Machine (JVM) or the Common Lan- guage Infrastructure (CLI), use static verification to ensure that no memory errors have been introduced inadvertently or maliciously before executing bytecode programs. Bytecodes, however, are still rather high-level compared to na- tive machine code. Runtime checks (e.g., array bounds checks) are built into otherwise potentially unsafe operations (e.g., mem- ory loads) to ease the verification process. To obtain acceptable performance, MRTEs compile programs using a just-in-time (JIT) compiler. A JIT compiler performs several control- and data-flow compiler transformations and produces optimized native machine code. In the process, runtime checks are often eliminated or sepa- rated from the potentially unsafe operations that they protect. As far as we are aware, all production Java and CLI JIT compilers remove safety information during the optimization process: optimized low level code or generated machine code is not easily verifiable. From a security perspective, this precludes the use of optimized low level code as a persistent and distributable format. Moreover, from a reli- ability perspective it requires that the user trust that complex com- piler transformations do not introduce memory errors. In recent years, researchers have developed proof languages (e.g., PCC [19] and TAL [18]) that allow a compiler to embed safety proofs into low-level code, along with verification tech- niques to validate those proofs. They have demonstrated certifying compilers that can compile Java and safe C-like languages [20, 8, 17, 13] while both performing optimizations and generating safety proofs. Nevertheless, although the proof language and verification process is well-developed, implementing or modifying existing op- timizations to correctly generate and/or preserve safety information is still an arduous and poorly understood process. POPL ’06 Submission 1 2005/11/15
  • 2. In this paper, we introduce a new program representation frame- work for safe, imperative, object-oriented languages to aid in the generation, propagation, and verification of safety information through aggressive compiler optimization. In this representation we encode safety dependences, the dependences between poten- tially unsafe operations and the control points that guarantee their safety, as abstract proof variables. These proof variables are purely static: they have no runtime semantics. Nevertheless, they are first class constructs produced by control points and consumed by po- tentially unsafe instructions. From the perspective of most compiler transformations, they are the same as any other variable. We argue that this representation is particularly well-suited to use as an intermediate representation for an aggressively optimiz- ing compiler. We demonstrate that it supports common advanced compiler optimizations without artificially constraining or exten- sively modifying them. In particular, we demonstrate that by carry- ing proof values in normal variables a compiler can leverage exist- ing transformations such as SSA construction, copy propagation, and dead code elimination to place, update and eliminate proof variables. We illustrate our ideas in the context of the machine-independent global optimizer of STARJIT [1], a dynamic optimizing compiler for Java and C#. STARJIT was designed as a high-performance op- timizing compiler and is competitive in performance with the best production MRTE systems. We describe a prototype integration of our ideas into STARJIT’s internal representation, and we discuss how it is able to preserve safety information through a varied set of aggressive optimizations. The original motivation for the safety dependence representation described in this paper was for opti- mization rather than safety. However, a prototype implementation of a verifier has also been developed, and this paper is intended to provide both a description of the safety dependence mechanism and a theoretical development of a type system based upon it. In particular, our paper makes the following contributions: 1. We introduce a safe low-level imperative program representa- tion that combines static single-assignment (SSA) form with explicit safety dependences, and we illustrate how it can be used to represent highly optimized code. 2. We present a simple type system to verify memory safety of programs in this representation. To the best of our knowledge, this type system is the first to formalize type checking in an SSA representation. While SSA is in some sense equivalent to CPS, the details are sufficiently different that our type system is quite unlike the usual lambda-calculus style type systems and required new proof techniques. 3. We demonstrate the utility of this program representation in a high-performance compiler, and we describe how a compiler can leverage its existing framework to preserve safety informa- tion. In particular, we demonstrate that only optimizations that directly affect memory safety, such as bounds check elimination and strength reduction of address calculations, require signifi- cant modification. The remainder of the paper is organized as follows. In Section 2, we motivate the explicit representation of safety dependence in an optimizing compiler and describe how to do this via proof variables in a low-level imperative program representation. In Section 3, we describe a formal core language specifically dealing with array- bounds checks and present a type system with which we can verify programs in SSA form. In Section 4, we demonstrate how a com- piler would lower a Java program to the core language and illustrate how aggressive compiler optimizations produce efficient and veri- fiable code. In Section 5, we informally describe extensions to our core language to capture complete Java functionality. In Section 6, if (a!=null) while (!done) { b = (B)a; · · · = · · · b.x · · · · · · } Figure 1. Field load in loop we discuss the status of our current implementation, and, finally, in Sections 7 and 8 we discuss related work and conclude. 2. Motivation We define a potentially unsafe instruction as any instruction that, taken out of context, might fault or otherwise cause an illegal memory access at runtime. Some instructions, taken independently, are inherently unsafe. A load instruction may immediately fault if it accesses protected memory or may trigger an eventual crash by reading an incorrectly typed value. A store may corrupt memory with an illegal value (e.g., if an arbitrary integer replaces an object’s virtual table). Consider, for example, the field access in Figure 1. Assuming C++-like semantics, the operation b.x dereferences memory with no guarantee of safety. In general, C++ does not guarantee that b refers to a real object of type B: b may hold an an integer that faults when used as a pointer. Assuming Java semantics, however, the field access itself checks at runtime that b does not point to a null location. If the check succeeds, the field access executes the load; otherwise, it throws an exception, bypassing the load. By itself, this built-in check does not ensure safety: the load also depends on the preced- ing cast, which dynamically checks that the runtime type of b is in fact compatible with the type B. If the check succeeds, the cast executes the load; otherwise, it throws an exception, bypassing the load. Typically, the safety of a potentially unsafe instruction depends on a set of control flow points. We refer to this form of dependence as safety dependence. In this example, the safety of the load de- pends on the cast that establishes its type. We call an instruction contextually safe when its corresponding safety dependences guar- antee its safety. To verify the output of a compiler optimization, we must prove that each instruction is contextually safe. 2.1 Safety In Java In Java and the verifiable subset of CLI, a combination of static ver- ification and runtime checks guarantee the contextual safety of indi- vidual bytecode instructions. Static type checking establishes that variables have the appropriate primitive or object type. Runtime checks such as type tests (for narrowing operations), null pointer tests, and array bounds tests detect conditions that would cause a fault or illegal access and throw a language-level runtime excep- tion instead. Figure 2 shows Java-like bytecode instructions (using pseudo- registers in place of stack locations for clarity) for the code of Figure 1. The Java type system guarantees that variable b has type B at compile time, while the getfield instruction guarantees non- null access by testing for null at runtime. The check and the static verifier together guarantee that the load operation will not trigger an illegal memory access. 2.2 Safety in a Low-Level Representation The Java bytecode format was not intended to be an intermedi- ate program representation for an optimizing compiler. There are a number of reasons why such a format is not suitable, but here we POPL ’06 Submission 2 2005/11/15
  • 3. ifnull a goto EXIT L : ifeq done goto EXIT b := checkcast(a, B) t1 := getfield(b, B::x) · · · goto L EXIT : Figure 2. Field load with Java-like bytecode if a = null goto EXIT L : if done = 0 goto EXIT checkcast(a, B) checknull(a) t2 := getfieldaddr(a, B::x) t1 := ld(t2) · · · goto L EXIT : Figure 3. Field load lowered in erasure-style representation will focus only on those related to safety. First, bytecodes hide re- dundant check elimination opportunities. For example, in Figure 2, optimizations can eliminate the null check built into the getfield instruction because of the ifnull instruction. Even though sev- eral operations have built-in exception checks, programmers usu- ally write their code to ensure that these checks never fail, so such optimization opportunities are common in Java programs. Second, extraneous aliasing introduced to encode safety prop- erties hides optimization opportunities. In Figures 1 and 2, vari- able b represents a copy of a that has the type B. Any use of a that requires this type information must use b instead. While this helps static verification, it hinders optimization. The field access must establish that b is not null, even though the ifnull statement establishes that property on a. To eliminate the extra check, a re- dundancy elimination optimization must reason about aliasing due to cast operations; this is beyond the capabilities of standard algo- rithms [16, 5]. In the absence of a mechanism for tracking safety dependences, STARJIT would lower a code fragment like this to one like that in Figure 3. Note that the ld operation is potentially unsafe and is safety dependent on the null check. In this case, however, the safety dependence between the null check and the load is not explicit. Al- though the instructions are still (nearly) adjacent in this code, there is no guarantee that future optimizations will leave them so. Fig- ure 4 roughly illustrates the code that STARJIT would produce for our example. Redundant checks are removed by a combination of partial loop peeling (to expose redundant control flow) and com- mon subexpression elimination. The invariant address field calcu- lation is hoisted via code motion. In this case, the dependence of the load on the operations that guarantee its safety (specifically, the if and checkcast statements) has become obscured. We refer to this as an erasure-style low-level representation, as safety information is effectively erased from the program. An alternative representation embeds safety information di- rectly into the values and their corresponding types. The Java lan- guage already does this for type refinement via cast operations. This approach also applies to null checks, as shown in Figure 5. The SafeTSA representation takes this approach, extending it to array bounds checks [24, 2] as well. We refer to this as a refinement-style representation. In this representation, value dependences preserve the safety dependence between a check and a load. To preserve t2 := getfieldaddr(a, B::x) if a = null goto EXIT if done = 0 goto EXIT checkcast(a, B) L : t1 := ld(t2) · · · if done 6= 0 goto L EXIT : Figure 4. Field load optimized in erasure-style representation if a = null goto EXIT L : if done = 0 goto EXIT b := checkcast(a, B) t3 := checknull(b) t2 := getfieldaddr(t3, B::x) t1 := ld(t2) · · · goto L EXIT : Figure 5. Field load lowered in refinement-style representation safety, optimizations must preserve the value flow between the check and the load. Check elimination operations (such as the checknull in Figure 5) may be eliminated by optimization, but the values they produce (e.g., t2) must be redefined in the process. From an optimization standpoint, a refinement-style represen- tation is not ideal. The safety dependence between the check and the load is not direct. Instead, it is threaded through the address field calculation, which is really just an addition operation. While the load itself cannot be performed until the null test, the address calculation is always safe. A code motion or instruction scheduling compiler optimization should be free to move it above the check if it is deemed beneficial. In Figure 3, it is clearly legal. In Figure 5, it is no longer possible. The refinement-style representation adds artificial constraints to the program to allow safety to be checked. In this case, the address calculation is artificially dependent on the check operation. A refinement-style representation also obscures optimization opportunities by introducing multiple names for the same value. Optimizations that depend on syntactic equivalence of expressions (such as the typical implementation of redundancy elimination) be- come less effective. In Figure 3, a is syntactically compared to null twice. In Figure 5, this is no longer true. In general, syntac- tically equivalent operations in an erasure-style representation may no longer be syntactically equivalent in a refinement-style repre- sentation. 2.3 A Proof Passing Representation Neither the erasure-style nor refinement-style representations pre- cisely represent safety dependences. The erasure-style representa- tion omits them altogether, while the refinement-style representa- tion encodes them indirectly. As a result, the erasure-style rep- resentation is easy to optimize but difficult to verify, while the refinement-style is difficult to optimize but easy to verify. To bridge this gap, we propose the use of a proof passing representation that encodes safety dependence directly into the program representation through proof variables. Proof variables act as capabilities for unsafe operations (similar to the capabilities of Walker et al. [25]). The availability of a proof variable represents the availability of a proof that a safety property holds. A potentially unsafe instruction must use an available proof variable to ensure POPL ’06 Submission 3 2005/11/15
  • 4. [s1, s2] if a = null goto EXIT L : if done = 0 goto EXIT s3 := checkcast(a, B) s4 := checknull(a) t2 := getfieldaddr(a, B::x) s5 := pfand(s3, s4) t1 := ld(t2) [s5] · · · goto L EXIT : Figure 6. Field load lowered in a proof passing representation t2 := getfieldaddr(a, B::x) [s1, s2] if a = null goto EXIT L : if done = 0 goto EXIT s3 := checkcast(a, B) s4 := s1 s5 := pfand(s3, s4) t1 := ld(t2) [s5] · · · goto L EXIT : Figure 7. Field load with CSE and Code Motion contextual safety. This methodology relates closely to mechanisms proposed for certified code by Crary and Vanderwaart [10] and Shao et al. [22] in the context of the lambda calculus. We discuss the relationship of our approach to this work in Section 7. Proof variables do not consume any physical resources at run- time: they represent abstract values and only encode safety de- pendences. Nevertheless, they are first-class constructs in our rep- resentation. They are generated by interesting control points and other relevant program points, and consumed by potentially unsafe instructions as operands guaranteeing safety. Most optimizations treat proof variables like other program variables. Figure 6 demonstrates how we represent a load operation in a proof passing representation. As in Figure 5, we represent safety through value dependences, but instead of interfering with existing values, we insert new proof variables that directly model the safety dependence between the load and both check operations. Figures 7 to 10 represent the relevant transformations performed by STARJIT to optimize this code. In Figure 7, we illustrate two op- timizations. First, STARJIT’s common subexpression elimination pass eliminates the redundant checknull operation. When STAR- JIT detects a redundant expression in the right hand side of an in- struction, it replaces that expression with the previously defined variable. The if statement defines the proof variable s1 if the test fails. This variable proves the proposition a 6= null. At the defi- nition of s4, the compiler detects that a 6= null is available, and redefines s4 to be a copy of s1. STARJIT updates a redundant proof variable the same way as any other redundant variable. Second, STARJIT hoists the definition of t2, a loop invariant address calculation, above the loop. Even though the computed ad- dress may be invalid at this point, the address calculation is always safe; we require a proof of safety only on a memory operation that dereferences the address. Figure 8 shows a step of copy propagation, which propagates s1 into the load instruction and eliminates the use of s4, allowing dead code elimination to remove the definition of s4. Figure 9 illustrates the use of partial loop peeling to expose re- dundant control flow operations within the loop. This transforma- t2 := getfieldaddr(a, B::x) [s1, s2] if a = null goto EXIT L : if done = 0 goto EXIT s3 := checkcast(a, B) s5 := pfand(s3, s1) t1 := ld(t2) [s5] · · · goto L EXIT : Figure 8. Field load with Copy Propagation t2 := getfieldaddr(a, B::x) [s1, s2] if a = null goto EXIT if done = 0 goto EXIT s1 3 := checkcast(a, B) L : s2 3 := φ(s1 3, s3 3) s5 := pfand(s2 3, s1) t1 := ld(t2) [s5] · · · if done = 0 goto EXIT s3 3 := checkcast(a, B) goto L EXIT : Figure 9. Field load with Partial Loop Peeling t2 := getfieldaddr(a, B::x) [s1, s2] if a = null goto EXIT if done = 0 goto EXIT s3 := checkcast(a, B) s5 := pfand(s3, s1) L : t1 := ld(t2) [s5] · · · if done 6= 0 goto L EXIT : Figure 10. Field load with 2nd CSE and Branch Reversal tion duplicates the test on done and the checkcast operation, and makes the load instruction the new loop header. The proof variable s3 is now defined twice, where each definition establishes that a has type B on its corresponding path. The compiler leverages SSA form to establish that the proof variable is available within the loop. Finally, in Figure 10, another pass of common subexpression elimination eliminates the redundant checkcast. Copy propaga- tion propagates the correct proof variable, this time through a re- dundant phi instruction. Note, that this final code is equivalent to the erasure-style representation in Figure 4 except that proof vari- ables provide a direct representation of safety. In Figure 10, it is readily apparent that the if and checkcast statements establish the safety of the load instruction. In the next section we formalize our approach as a small core language, and the following sections show its use and preservation across compiler optimizations and extension to full Java. 3. Core Language In this section we describe a small language that captures the main ideas of explicit safety dependences through proof variables. As usual with core languages, we wish to capture just the essence of the problem and no more. The issue at hand is safety dependences, and to keep things simple we will consider just one such depen- POPL ’06 Submission 4 2005/11/15
  • 5. (P, L1, n1, b.i) 7→ (P, L2, n2, pc) where: P(b.i) L2 n2 pc Side conditions p L1{x1 := L1(x2)} n1 b.(i + 1) p[n1] = x1 := x2 x : τ := i L1{x := i} n1 b.(i + 1) x1 : τ := x2 L1{x1 := L1(x2)} n1 b.(i + 1) x1 : τ := newarray(x2, x3) L1{x1 := v1} n1 b.(i + 1) L1(x2) = n, L1(x3) = v3, v1 = hv3, . . . , v3 | {z } n i x1 : τ := newarray(x2, x3) L1{x1 := v1} n1 b.(i + 1) L1(x2) = i, i < 0, v1 = hi x1 : τ := len(x2) L1{x1 := n} n1 b.(i + 1) L1(x2) = hv0, . . . , vn−1i x1 : τ := base(x2) L1{x2 := v@0} n1 b.(i + 1) L1(x2) = v, v = hv′i x1 : τ := x2 bop x3 L1{x1 := i4} n1 b.(i + 1) L1(x2) = i2, L1(x3) = i3, i4 = i2 bop i3 x1 : τ := x2 bop x3 L1{x1 := v@i4} n1 b.(i + 1) L1(x2) = v@i2, L1(x3) = i3, i4 = i2 bop i3 x1 : τ := ld(x2) [x3] L1{x1 := vi} n1 b.(i + 1) L1(x2) = hv0, . . . , vni@i, 0 ≤ i ≤ n x1 : τ := pffact(x2) L1{x1 := true} n1 b.(i + 1) x : τ := pfand(y) L1{x := true} n1 b.(i + 1) [x1 : τ1, x2 : τ2] if x3 rop x4 goto b′ L1{x1 := true} edgeP (b, b + 1) (b + 1).0 L1(x3) = i3, L1(x4) = i4, ¬(i3 rop i4) [x1 : τ1, x2 : τ2] if x3 rop x4 goto b′ L1{x2 := true} edgeP (b, b′) b′.0 L1(x3) = i3, L1(x4) = i4, i3 rop i4 goto b′ L1 edgeP (b, b′) b′.0 Figure 11. Operational semantics dence, namely, bounds checking for arrays. In particular, we con- sider a compiler with separate address arithmetic, load, and store operations, where the type system must ensure that a load or store operation is applied only to valid pointers. Moreover, since the ba- sic safety criteron for a store is the same as for a load, namely, that the pointer is valid, we consider only loads; adding stores to our core language adds no interesting complications. Not considering stores further allows us to avoid modelling the heap explicitly, but to instead use a substitution semantics which greatly simplifies the presentation. The syntax of our core language is given as follows: Prog. States S ::= (P, L, n, pc) Programs P ::= B Blocks B ::= p; ι; c Phi Instructions p ::= x : τ := φ(x) Instructions ι ::= x : τ := r Right-hand sides r ::= i | x | newarray(x1, x2) | len(x) | base(x) | x1 bop x2 | ld(x1) [x2] | pffact(x) | pfand(x) Binary Ops bop ::= + | − Transfers c ::= goto n | halt | [x1 : τ1, x2 : τ2] if x3 rop x4 goto n Relations rop ::= <|≤|=|6= Environments L ::= x := v Values v ::= i | hvi | hvi@i | true Prog. Counters pc ::= n1.n2 Here i ranges over integer constants, x ranges over variables, n ranges over natural numbers, and φ is the phi-operation of SSA. We use the bar notation introduced in Featherweight Java [15]: B abbreviates B0, . . . , Bn, x := v abbreviates x0 := v0, . . . , xn := vn, et cetera. We also use the bar notation in type rules to ab- breviate a sequence of typing judgements in the obvious way. In addition to the grammar above, programs are subject to a number of context-sensitive restrictions. In particular, the n in [x1 : τ1, x2 : τ2] if x3 rop x4 goto n and goto n must be a block number in the program (i.e., if the program is B0, . . . , Bm then 0 ≤ n ≤ m); the transfer in the last block must be a goto or halt; the number of variables in a phi instruction must equal the number of incoming edges (as defined below) to the block in which it appears; the variables assigned in the phi instructions of a block must be distinct. Informally, the key features of our language are the following. The operation base(x) takes an array and creates a pointer to the element at index zero. The arithmetic operations can be applied to such pointers and an integer to compute a pointer to a different in- dex. The ld(x1) [x2] operation loads the value pointed to by the pointer in x1. The variable x2 is a proof variable and conceptually contains a proof that x1 is a valid pointer: that is, that it points to an in-bounds index. The typing rules ensure that x1 is valid by requir- ing x2 to contain an appropriate proof. The operations pffact(x) and pfand(x) construct proofs. For pffact(x) a proof of a for- mula based on the definition of x is constructed. For example, if x’s definition is x : int := len(y) then pffact(x) constructs a proof of x = len(y). A complete definition of the defining facts of instructions appears in Figure 14. For pfand(x1, . . . , xn), x1 through xn are also proof variables, and a proof of the conjunction is returned. Values of the form hv0, . . . , vni@i represent pointers to array elements: in this case a pointer to the element at index i of an array of type hv0, . . . , vni. Such a pointer is valid if i is in bounds (that is, if 0 ≤ i ≤ n) and invalid otherwise. The typing rules must ensure that only valid pointers are loaded from, with proof variables used to provide evidence of validity. The final un- usual aspect of the language is that branches assign proofs to proof variables that reflect the condition being branched on. For exam- ple, in the branch [x1 : τ1, x2 : τ2] if x3=x4 goto n, a proof of x3 6= x4 is assigned to x1 along the fall-through edge, and a proof of x3 = x4 is assigned to x2 along the taken edge. These proofs can then be used to discharge validity requirements for pointers. To state the operational semantics and type system we need a few definitions. The program counters of a program pcs(P) are {b.i | P = B0, . . . , Bm ∧ b ≤ m ∧ Bb = p; ι1; · · · ; ιn; c ∧ i ≤ n+1}. We write P(b) for Bb when P = B0, . . . , Bn and b ≤ n; if P(b) = p; ι1; . . . ; ιm; c then P(b.n) is p when n = 0, and ιn when 1 ≤ n ≤ m and c when n = m + 1. The edges of a program P, edges(P), are as follows. The entry edge is (−1, 0). If P(n) ends in [x1 : τ1, x2 : τ2] if x3 rop x4 goto n′ then there are edges (n, n+1), called the fall-through edge, and (n, n′ ), called the taken edge. If P(n) ends in goto n′ then there is an edge (n, n′ ). For a given P and n2 the edges (n1, n2) ∈ edges(P) are numbered from zero in the order given by n1; edgeP (n1, n2) is this number, also called the incoming edge number of (n1, n2) into n2. Operational Semantics A program P is started in the state (P, ∅, 0, 0.0). The reduction relation that maps one state to the next is given in Figure 11. Note that the third component of a pro- POPL ’06 Submission 5 2005/11/15
  • 6. gram state tracks which incoming edge led to the current program counter—initially this is the entry edge (−1, 0), and is updated by transfers. It is used by phi instructions to select the correct variable. The notation p[i] denotes x1 := x1i, . . . , xn := xni when p = x1 : τ1 := φ(x11, . . . , x1m), . . . , xn : τn := φ(xn1, . . . , xnm). A program terminates when in a state of the form (P, L, n, pc) where P(pc) = halt. A program state is stuck if it is irreducible and not a terminal state. Stuck states all represent type errors that the type system should prevent. Note that the array creation opera- tion must handle negative sizes. Our implementation would throw an exception, but since the core language does not have exceptions, it simply creates a zero length array if a negative size is requested. In the operational semantics, the proof type has the single in- habitant true, upon which no interesting operations are defined. Proofs in this sense are equivalent to unit values for which non- escaping occurrences can be trivially erased when moving to an untyped setting. This “proof erasure” property is precisely analo- gous to the “coercion erasure” property of the coercion language of Vanderwaart et al. [23]. In practice, uses of proof variables in the STARJIT compiler are restricted such that all proof terms can be elided during code generation and consequently impose no over- head at run time. While we believe that it would be straightforward to formalize the syntactic restrictions that make this possible, we choose for the sake of simplicity to leave this informal here. Type System The type system has two components: the SSA property and a set of typing judgements. The SSA property ensures both that every variable is assigned to at most once in the program text (the single assignment property) and that all uses of variables are dominated by definitions of those variables. In a conventional type system, these properties are enforced by the typing rules. In particular, the variables that are listed in the context of the typing judgement are the ones that are in scope. For SSA IRs, it is more convenient to check these properties separately. The type checker must ensure that during execution each use of a variable is preceded by an assignment to that variable. Since the i- th variable of a phi instruction is used only if the i-th incoming edge was used to get to the block, and the proof variables in an if transfer are assigned only on particular out-going edges, we give a rather technical definition of points at which variables are assigned or used. These points are such that a definition point dominating a use point implies that assignment will always precede use. These points are based on an unconventional notion of control-flow graph, to avoid critical edges which might complicate our presentation. For a program P with blocks 0 to m, the control-flow graph consists of the nodes {0, . . . , m} ∪ edges(P) and edges from each original node n to each original edge (n, n′ ) and similarly from (n, n′ ) to n′ . The definition/use points, du(P), are pcs(P) ∪ {b.0.i | P(b.0) = p0, . . . , pn ∧ 0 ≤ i ≤ n} ∪ {e.i | e ∈ edges(P) ∧ i ∈ {0, 1}}. Figure 13 gives the formal definition of dominance, defini- tion/use points, and the SSA property. The syntax of types is: Types τ ::= int | array(τ) | ptr?hτi | S(x) | pf(F ) Facts F ::= e1 rop e2 | F1 ∧ F2 Fact Exps. e ::= i | x | len(x) | e1 bop e2 | x@e Environments Γ ::= x : τ The type ptr?hτi is given to pointers that, if valid, point to values with type τ (the ? indicates that they might not be valid). The singleton type S(x) is given to things that are equal to x. The type pf(F ) is given to proof variables that contain a proof of the fact F. Facts include arithmetic comparisons and conjunction. Fact expressions include integers, variables, array lengths, arithmetic operations, and a subscript expression—the fact expression x@e stands for a pointer that points to the element at index e of array x. Judgement Meaning Γ ⊢ τ1 ≤ τ2 τ1 is a subtype of τ2 in Γ ⊢ F1 =⇒ F2 F1 implies F2 Γ ⊢ p p is safe in environment Γ Γ ⊢P ι ι is safe in environment Γ Γ ⊢ c c is safe in environment Γ ⊢P τ at du τ well-formed type at du in P ⊢P Γ environment Γ well-formed in P ⊢ P P is safe Figure 12. Typing judgements The judgements of the type system are given in figure 12. Most of the typing rules are given in Figure 14. Typing environments Γ state the types that variables are supposed to have. The rules check that when assignments are made to a variable, the type of the assigned value is compatible with the variable’s type. For example, the judgement Γ ⊢ int ≤ Γ(x) in the rule for x : τ := i checks that integers are compatible with the type of x. The rules also check that uses of a variable have a type compatible with the operation. For example, the rule for load expects a proof that the pointer, x2, is valid, so the rule checks that x3’s type Γ(x3) is a subtype of pf(x@0≤x2∧x2<x@len(x)) for some x. It is this check along with the rules for proof value generation and the SSA property that ensure that x2 is valid. Given these remarks, the only other complicated rule is for phi instructions. In a loop a phi instruction might be used to combine two indices, and the compiler might use another phi instruction to combine the proofs that these indices are in bounds. For example, consider this sequence: x1 : int := φ(x2, x3) y1 : pf(0≤x1) := φ(y2, y3) where y2 : pf(0≤x2) and y3 : pf(0≤x3). Here the types for y1, y2, and y3 are different and in some sense incompatible, but are intuitively the correct types. The rule for phi instructions allows this typing. In checking that y2 has a compatible type, the rule substitutes x2 for x1 in y1’s type to get pf(0≤x2), which is the type that y2 has; similarly for y3. For a program P that satisfies the SSA property, every variable mentioned in the program has a unique definition point, and that definition point is decorated with a type. Let vt(P) denote the environment formed from extracting these variable/type pairs. A program P is well formed (⊢ P) if: 1. P satisfies the SSA property, 2. ⊢P vt(P), 3. vt(P) ⊢ p for every p in P, 4. vt(P) ⊢P ι for every instruction ι in P, and 5. vt(P) ⊢ c for every transfer c in P. The type system is safe: THEOREM 1 (Type Safety). If ⊢ P and (P, ∅, 0, 0.0) 7→∗ S then S is not stuck. A proof of this theorem is given in appendix A. The proof takes the standard high-level form of showing preservation and progress lemmas, as well as some lemmas particular to an SSA language. It is important to note that safety of the type system is contingent on the soundness of the decision procedure for ⊢ F1 =⇒ F2. In the proof, a judgement corresponding to truth of facts in an environment is given. In this setting, the assumption of logical soundness corresponds to the restriction that in any environment in which F1 is true, F2 is also true. POPL ’06 Submission 6 2005/11/15
  • 7. Defs and Uses: If P(b.i) = x : τ := r then program counter b.i defines x, furthermore, b.i is a use of the ys where r has the following forms: y | newarray(y1, y2) | len(y) | base(y) | y1 bop y2 | ld(y1) [y2] | pffact(y) | pfand(y) If P(b.i) = (p0, . . . , pn) and pj = xj : τj := φ(yj1, . . . , yjm) then b.i.j defines each xj and ek.1 uses each yjk where ek is the k-th incoming edge of b. If P(b.i) = [x1 : τ1, x2 : τ2] if y1 rop y2 goto n then e1.0 defines x1 and e2.0 defines x2 where e1 and e2 are the fall-through and taken edges respectively, and b.i uses y1 and y2. If x has a unique definition/use point in P that defines it, then defP (x) is this point. Dominance: • In program P, node n dominates node m, written domP (n, m), if every path in the control-flow graph of P from (−1, 0) to m includes n. • In program P, definition/use point n1.i1 strictly dominates definition/use point n2.i2, written sdomP (n1.i1, n2.i2) if n1 = n2 and i1 < i2 (here i1 or i2 might be a dotted pair 0.j, so we take this inequality to be lexicographical ordering) or n1 6= n2 and domP (n1, n2). Single Assignment: A program satisfies the single-assignment property if every variable is defined by at most one definition/use point in that program. In Scope: A program P satisfies the in-scope property if for every definition/use point du1 that uses a variable there is a definition/use point du2 that defines that variable and sdomP (du2, du1). SSA: A program satisfies the Single Static Assignment (SSA) property if it satisfies the single-assignment and in-scope properties. Note that a program that satisfies SSA has a unique definition for each variable mentioned in the program. Figure 13. SSA definitions ⊢P τ at du ⊢P Γ fv(τ) ⊆ inscopeP (du) ⊢P τ at du ⊢P τ at defP (x) ⊢P x : τ Γ ⊢ τ1 ≤ τ2 ⊢ F1 =⇒ F2 Γ ⊢ int ≤ int Γ ⊢ τ1 ≤ τ2 Γ ⊢ array(τ1) ≤ array(τ2) Γ ⊢ τ1 ≤ τ2 Γ ⊢ ptr?hτ1i ≤ ptr?hτ2i Γ ⊢ S(x) ≤ S(x) Γ ⊢ S(x) ≤ Γ(x) ⊢ F1 =⇒ F2 Γ ⊢ pf(F1) ≤ pf(F2) Γ ⊢ τ1 ≤ τ2 Γ ⊢ τ2 ≤ τ3 Γ ⊢ τ1 ≤ τ3 The judgement ⊢ F1 =⇒ F2 is some appropriate decision procedure for our fact language. Γ ⊢ p Γ ⊢P ι Γ ⊢ c Γ ⊢ S(xij) ≤ Γ(xi){x1, . . . , xn := x1j, . . . , xnj} Γ ⊢ x1 : τ1 := φ(x11, . . . , x1m), . . . , xn : τn := φ(xn1, . . . , xnm) Γ ⊢ int ≤ Γ(x) Γ ⊢P x : τ := i Γ ⊢ S(x2) ≤ Γ(x1) Γ ⊢P x1 : τ := x2 Γ ⊢ Γ(x2) ≤ int Γ ⊢ array(Γ(x3)) ≤ Γ(x1) Γ ⊢P x1 : τ := newarray(x2, x3) Γ ⊢ Γ(x2) ≤ array(τ2) Γ ⊢ int ≤ Γ(x1) Γ ⊢P x1 : τ := len(x2) Γ ⊢ Γ(x2) ≤ array(τ2) Γ ⊢ ptr?hτ2i ≤ Γ(x1) Γ ⊢P x1 : τ := base(x2) Γ ⊢ Γ(x2) ≤ int Γ ⊢ Γ(x3) ≤ int Γ ⊢ int ≤ Γ(x1) Γ ⊢P x1 : τ := x2 bop x3 Γ ⊢ Γ(x2) ≤ ptr?hτ2i Γ ⊢ Γ(x3) ≤ int Γ ⊢ ptr?hτ2i ≤ Γ(x1) Γ ⊢P x1 : τ := x2 bop x3 Γ ⊢ Γ(x2) ≤ ptr?hτ2i Γ ⊢ Γ(x3) ≤ pf(x@0≤x2∧x2<x@len(x)) Γ ⊢ τ2 ≤ Γ(x1) Γ ⊢P x1 : τ := ld(x2) [x3] Γ ⊢ pf(deffactP (x2)) ≤ Γ(x1) Γ ⊢P x1 : τ := pffact(x2) Γ ⊢ Γ(y1) ≤ pf(F1) · · · Γ ⊢ Γ(yn) ≤ pf(Fn) Γ ⊢ pf(F1∧···∧Fn) ≤ Γ(x1) Γ ⊢P x : τ := pfand(y1, . . . , yn) Γ ⊢ Γ(x3) ≤ int Γ ⊢ Γ(x4) ≤ int Γ ⊢ pf(¬(x3 rop x4)) ≤ Γ(x1) Γ ⊢ pf(x3 rop x4) ≤ Γ(x2) Γ ⊢ [x1 : τ1, x2 : τ2] if x3 rop x4 goto n Γ ⊢ goto n Γ ⊢ halt deffactP (x) The fact deffactP (x) depends upon the defining instruction of x in P, and is given by these rules: deffactP (x : τ := i) = x=i deffactP (x : τ := len(x′)) = x=len(x′) deffactP (x : τ := base(x′)) = x = x′@0 deffactP (x : τ := x1 bop x2) = x=x1 bop x2 Figure 14. Typing rules POPL ’06 Submission 7 2005/11/15
  • 8. The typing rules presented are for the most part syntax-directed, and can be made algorithmic. A consideration is that the rule for load must determine the actual array variable, which is not apparent from the conclusion. In general, the decision prodecure only needs to verify that the rule holds for one of the arrays available at that program point. In practice, the correct array can be inferred by ex- amining the type of the proof variable. We believe that judgements on facts may be efficiently decided by an integer linear program- ming tool such as the Omega Calculator [21] with two caveats. First, such tools reason over Z rather than 32- or 64-bit integers. Second, they restrict our fact language for integer relations (and, thus, compiler reasoning) to affine expressions. This is, however, sufficient to capture current STARJIT optimizations. 4. Compiler optimizations In this section we examine compiler optimizations in the context of the core language. We demonstrate how an optimizing compiler can preserve both proof variables and their type information. We argue that our ideas greatly simplify this process. In previous work, an im- plementer would need to modify each optimization to update safety information. In our representation, we leverage existing compiler infrastructure to do the bulk of the work. In particular, most control- flow or data-flow optimizations require virtually no changes at all. Others that incorporate algebraic properties only need to be modi- fied to record the compiler’s reasoning. In the next section we will discuss how these ideas can be extended from the core language to full Java. In general, there are two ways in which an optimization can maintain the correctness of the proofs embedded in the program. First, it can apply the transformation to both computation and proof simultaneously. This is sufficient for the majority of optimizations. Second, it can create new proofs for the facts provided by the original computation. As we show below, this is necessary for the few optimizations that infer new properties that affect safety. In the rest of this section we show how these general principles apply to individual compiler optimizations on a simple example. For this example, we show how to generate a low-level intermediate representation that contains safety information and how to preserve this information through several compiler optimizations, such as loop invariant code motion, common subexpression elimination, array bounds check elimination, strength reduction of array element pointer, and linear function test replacement. The example we will consider, in pseudo code, is: for (i=0; i<a.length; i++) { · · · = a[i]; } Where we assume that a is a non-null integer array, that a is not modified in the loop, and that the pseudo code array subscripting has an implicit bounds check. Although this example does not reflect the full complexity of Java, it is sufficient to illustrate the main ideas of propagating safety information through the compiler optimizations. Section 5 discusses additional issues in addressing full Java. The first compilation step for our example lowers the program into a low-level representation suitable for optimization, as shown in Figure 15. In our system, lowering generates instructions that ex- press the computation and any required proofs of the computation’s safety. For example, a typical compiler would expand an array ele- ment access a[i] into the following sequence: array bounds checks, computation of the array element address, and a potentially unsafe load from that address. In our system, the compiler also generates proof variables that show that the array index i is within the ar- ray bounds (q4 for the lower bound and q6 for the upper bound) and that the load accesses an element i of the array a (proof vari- i1 : int :=0 uB : int :=len(a) LOOP : i2 : int :=φ(i1, i3) [q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT aLen : int :=len(a) q3 : pf(aLen=len(a)) :=pffact(aLen) q4 : pf(0≤i2) :=checkLowerBound(i2, 0) q5 : pf(i2<aLen) :=checkUpperBound(i2, aLen) q6 : pf(i2<len(a)) :=pfand(q3, q5) aBase : ptr?hinti :=base(a) q7 : pf(aBase=a@0) :=pffact(aBase) addr : ptr?hinti :=aBase+i2 q8 : pf(addr=aBase+i2) :=pffact(addr) q9 : pf(addr=a@i2) :=pfand(q7, q8) q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9) val : int :=ld(addr) [q10] . . . : . . . :=val i3 : int :=i2+1 goto LOOP EXIT : . . . Figure 15. Low-level representation for array load in loop i1 : int :=0 uB : int :=len(a) q3 : pf(uB=len(a)) :=pffact(uB) aBase : ptr?hinti :=base(a) q7 : pf(aBase=a@0) :=pffact(aBase) LOOP : i2 : int :=φ(i1, i3) [q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT q4 : pf(0≤i2) :=checkLowerBound(i2, 0) q5 : pf(i2<uB) :=checkUpperBound(i2, uB) q6 : pf(i2<len(a)) :=pfand(q3, q5) addr : ptr?hinti :=aBase+i2 q8 : pf(addr=aBase+i2) :=pffact(addr) q9 : pf(addr=a@i2) :=pfand(q7, q8) q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9) val : int :=ld(addr) [q10] . . . : . . . :=val i3 : int :=i2+1 goto LOOP EXIT : . . . Figure 16. IR after CSE and loop invariant code motion able q9). The conjunction of these proofs is sufficient to type check the load instruction according to the typing rules in Figure 14. The proof variables are generated by the explicit array bounds checks (which we use as syntactic sugar for the branches that transfer con- trol to a halt instruction if the bounds check fails) and by pffact and pfand statements that encode arithmetic properties of the ad- dress computation as the types of proof variables. Next, we take the example in Figure 15 through several common compiler optimizations that are employed by STARJIT to generate efficient code for loops iterating over arrays (Figures 16 - 19). The result is highly-optimized code with an embedded proof of program safety. We start, in Figure 16, by applying several basic data-flow op- timizations such as CSE, dead code elimination, and loop invari- ant code motion. An interesting property of these optimizations in our system is that they require no modification to preserve the POPL ’06 Submission 8 2005/11/15
  • 9. i1 : int :=0 q11 : pf(i1=0) :=pffact(i1) uB : int :=len(a) q3 : pf(uB=len(a)) :=pffact(uB) aBase : ptr?hinti :=base(a) q7 : pf(aBase=a@0) :=pffact(aBase) LOOP : i2 : int :=φ(i1, i3) q4 : pf(0≤i2) :=φ(q11, q13) [q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT q6 : pf(i2<len(a)) :=pfand(q3, q1) addr : ptr?hinti :=aBase+i2 q8 : pf(addr=aBase+i2) :=pffact(addr) q9 : pf(addr=a@i2) :=pfand(q7, q8) q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9) val : int :=ld(addr) [q10] . . . : . . . :=val i3 : int :=i2+1 q12 : pf(i3=i2+1) :=pffact(i3) q13 : pf(0≤i3) :=pfand(q4, q12) goto LOOP EXIT : . . . Figure 17. IR after bound check elimination safety proofs. They treat proof variables identically to other terms, and, thus, are automatically applied to both the computation and the proofs. For example, common subexpression elimination and copy propagation replace all occurrences of aLen with uB, includ- ing those that occur in proof types. The type of the proof variable q3 is updated to match its new definition pffact(uB). In Figure 17, we illustrate array bounds check elimination. In the literature [4], this optimization is typically formulated to re- move redundant bounds checks without leaving any trace of its rea- soning in the program. In such an approach, a verifier must effec- tively repeat the optimization reasoning to prove program safety. In our system, an optimization cannot eliminate an instruction that de- fines a proof variable without constructing a new definition for that variable or removing all uses of that variable. Intuitively, the com- piler must record in a new definition its reasoning about why the eliminated instruction was redundant. Consider the bounds checks in Figure 16. The lower bound check that verifies that 0≤i2 is redundant because i2 is a monotonically increasing variable with the initial value 0. Formally, the facts that i1=0, i2=φ(i1, i3) and i3=i2+1 imply that 0≤i2. This reasoning is recorded in the trans- formed program through a new definition of the proof variable q4 and the additional proof variables q11 and q13. We use SSA to con- nect these proofs at the program level. The upper bound check that verifies that i2<len(a) (proof variable q5) is redundant because the if statement guarantees the same condition (proof variable q1). Because the new proof for the fact q5 is already present in the pro- gram, the compiler simply replaces all uses of of q5 with q1. In Figure 18, we perform operator strength reduction (OSR) [9] to find a pointer that is an affine expression of a monotonically in- creasing or decreasing loop index variable and to convert it into an independent induction variable. In our example, OSR eliminates i from the computation of addr by incrementing it directly. Because variable addr is used in the q8 := pffact(addr) statement, the compiler cannot modify the definition of addr without also mod- ifying the definition of q8 (otherwise, the transformed program would not type check). Informally, the compiler must reestablish the proof that the fact trivially provided by the original definition still holds. In our system, OSR is modified to construct a new proof for the fact trivially implied by the original pointer definition by i1 : int :=0 q11 : pf(i1=0) :=pffact(i1) uB : int :=len(a) q3 : pf(uB=len(a)) :=pffact(uB) aBase : ptr?hinti :=base(a) q7 : pf(aBase=a@0) :=pffact(aBase) addr1 : ptr?hinti :=aBase+i1 q14 : pf(addr1=aBase+i1) :=pffact(addr1) LOOP : i2 : int :=φ(i1, i3) q4 : pf(0≤i2) :=φ(q11, q13) addr2 : ptr?hinti :=φ(addr1, addr3) q8 : pf(addr2=aBase+i2) :=φ(q14, q16) [q1 : pf(i2<uB), q2 : . . .] := if uB≤i2 goto EXIT q6 : pf(i2<len(a)) :=pfand(q3, q1) q9 : pf(addr2=a@i2) :=pfand(q7, q8) q10 : pf(a@0≤addr<a@len(a)) :=pfand(q4, q6, q9) val : int :=ld(addr2) [q10] . . . : . . . :=val i3 : int :=i2+1 q12 : pf(i3=i2+1) :=pffact(i3) addr3 : ptr?hinti :=addr2+1 q15 : pf(addr3=addr2+1) :=pffact(addr3) q13 : pf(0≤i3) :=pfand(q4, q12) q16 : pf(addr3=aBase+i3) :=pfand(q8, q12, q15) goto LOOP EXIT : . . . Figure 18. IR after strength reduction of element address uB : int :=len(a) q3 : pf(uB=len(a)) :=pffact(uB) aBase : ptr?hinti :=base(a) q7 : pf(aBase=a@0) :=pffact(aBase) addr1 : ptr?hinti :=aBase q14 : pf(addr1=aBase) :=pffact(addr1) addrUB : ptr?hinti :=aBase+uB q17 : pf(addrUB=aBase+uB) :=pffact(addrUB) LOOP : addr2 : ptr?hinti :=φ(addr1, addr3) q4 : pf(aBase≤addr2) :=φ(q14, q13) [q1 : pf(addr2<addrUB), q2 : . . .] := if addrUB≤addr2 goto EXIT q6 : pf(addr2<aBase+len(a)) :=pfand(q3, q1, q17) q10 : pf(a@0≤addr2<a@len(a)) :=pfand(q4, q6, q7) val : int :=ld(addr2) [q10] . . . : . . . :=val addr3 : ptr?hinti :=addr2+1 q15 : pf(addr3=addr2+1) :=pffact(addr3) q13 : pf(aBase≤addr3) :=pfand(q4, q15) goto LOOP EXIT : . . . Figure 19. IR after linear function test replacement induction on that fact. Again, we leverage SSA to establish the new proof. In this case, q8 : pf(addr2=aBase+i2) is defined by the phi in- struction that merges proof variables q14 : pf(addr1=aBase+i1) and q16 : pf(addr3=aBase+i3). POPL ’06 Submission 9 2005/11/15
  • 10. Finally, we illustrate linear function test replacement (LFTR) [9] in Figure 19. 1 Classical LFTR replaces the test uB≤i2 in the branch by a new test addrUB≤addr2. If our program contained no proof variables, this would allow the otherwise unused base variable i to be removed from the loop. We augment the usual LFTR pro- cedure, which rewrites occurrences of the base induction variable i2 in loop exit tests (and exits) in terms of the derived induction variable addr2, to also rewrite occurrences of i2 in the types of proof variables. Finally, to eliminate the original induction variable altogether, the compiler must replace the inductive proofs on the original variable (expressed through φ instructions) with proofs in terms of the derived induction variable. In this case, the compiler must replace the proof that 0≤i2 (established by q11 and q12) with one that proves aBase≤addr2 (established by q14 and q15). Af- ter the replacement, the loop induction variable i and any proof variables that depend upon it are no longer live in the loop, so all definitions of the variable can be removed. The compiler must re- move the proof variables whose types reduce to tautologies and apply further CSE to yield Figure 19. 5. Extensions Our core language can easily be extended to handle other interest- ing aspects of Java and CLI. In this section we describe several of these extensions. Firstly, we can handle object-model lowering through the use of our singleton types. Consider an invoke virtual operation. It is typically lowered into three operations: load the virtual dispatch table (vtable), load the method pointer from the vtable, call the method pointer passing the object as an additional argument. In our system, these operations would look like this: x : SomeClass := · · · t1 : vtable(x) := vtable(x) t2 : (S(x), int) → int := method(foo : (int) → int, t1) t3 : int := call(t2)(x, 10) Here the method foo (taking an integer and returning an integer) is being invoked on variable x. In the lowered code, variable t1 gets the dependent type vtable(x) meaning that it contains the vtable from the object currently in x. Variable t2 gets the loaded method pointer. From the type vtable(x), the typing rules can determine a precise function type for this method pointer, namely (S(x), int) → int, where the first argument must be x. The actual call is the last operation, and here we pass x as an explicit argument. Since x has type S(x), this operation type checks. By using singleton types based on term variables, we achieve a relatively simple type system and still avoid the well known typing problems with the explicit “this” argument (see [12] and references). The existing solutions to this typing problem have much more complicated type systems, with one exception. Chen and Tarditi [7] have a similarly simple type system for a lowered IR for class-based object-oriented languages. Like our system, theirs also has class names as types, and keeps around information about the class hierarchy, fields, and methods. They also have existentials with subclass bounds (type variables can be bounded above by a class, and range over any subclass of that class). They use these existentials to express the unknown runtime type of any given object, and thus the type of the explicit “this” argument. They also have a class representation function that maps class names 1 Note that the code resulting from LFTR is not typable in our core lan- guage, since we do not allow conditional branches on pointers. Extending the language to handle this is straightforward, but requires a total ordering on pointer values which essentially requires moving to a heap-based seman- tics. Note though that the fact language does permit reasoning about pointer comparison, as used in the previous examples. to a record type for objects in the class, and they have coercions to convert between the two. These ideas could be adapted to our system instead of our vtable types, and our vtable types could be adapted to their type system. In summary, both systems are simpler than existing, more foundational, object encodings. Theirs has type variables and bounded existentials, ours has singleton types based on term variables. Java and CLI also allow null as a value in any class type, and at runtime this null value must be checked and an exception thrown before any invocation or field access on an object. We can use our proof variable technique to track and ensure that these null checks are done. We simply add a null constant to the fact expression lan- guage. We can add an operation like p : pf(x6=null) := chknull(x) to check that x is not null. If x is null then it throws an exception, if not then it assigns a proof of x6=null to p. Similarly to array- bounds check elimination, we can eliminate redundant null checks. To handle exceptions we simply add explicit control flow for them. Each potentially exception throwing operation will end a ba- sic block and there will be edges coming out of the block corre- sponding to exceptions that go to blocks corresponding to the ex- ception handlers. An important point is that exceptions typically occur before the assignment of the potentially exception throw- ing operation, so like the conditional branches of our core lan- guage, we must treat the definition point as occuring on the fall- through edge rather than at the end of the basic block. So in both x : τ := chknull(y) and x : τ := call(y)(y), the variable x is assigned on the fall-through edge. We can easily deal with stores to pointers by adding a store operation of the form st(x, y) [p] where x holds the pointer, y the value to store, and p a proof that x is valid. The type rule for this operation is: Γ ⊢ Γ(x) ≤ ptr?hτi Γ ⊢ Γ(y) ≤ τ Γ ⊢ Γ(p) ≤ pf(z@0≤x∧x<z@len(z)) Γ ⊢P st(x, y) [p] Modifying our formalisation and type soundness proof to accomo- date stores would be straightforward. Java and CLI have mutable covariant arrays, and thus require array-store checks at runtime. In particular, when storing into an array, the runtime must check that the object being stored is com- patible with the runtime element type of the array (which could be a subtype of the static element type). In our implementation we use types of the form elem(x) to stand for the runtime element type of array x. The load base operation on x actually returns something of type ptr?helem(x)i. The array-store check produces a proof value that can be used to prove that some other variable has type elem(x) and we have a coercion to use the proof value to change the vari- able’s type. The end of a lowered array store would look something like this: x : array(C) := · · · y : C := · · · · · · p1 : pf(x6=null∧x@0≤t∧t<x@len(x)) := · · · p2 : pf(y:elem(x)) := chkst(x, y) st(t, retype(y, p2)) [p1] One technicality is worth noting. In order to avoid circularities between the type system and the fact language, and to avoid making the fact language’s decision procedure mutually dependent upon the subtype checker, we restrict the types that can appear in a fact of the form x : τ to those that do not mention proof types. Downcasts are similar to store checks, and we can treat them in a similar way. A chkcast(x : C) operation checks that x is in type C and returns a proof of this fact, otherwise it throws an exception. The actual subtype checks performed at runtime in our implementa- POPL ’06 Submission 10 2005/11/15
  • 11. tion are generally done by the virtual machine itself, and the virtual machine is not type checked by the type system of our JIT. How- ever, we do partially inline this operation to include some common fast cases, and to expose some parts to redundant elimination and CSE. For example, if a object is null then it is in any reference type and can be stored into any reference array or downcast to any ref- erence type. Another example is comparing the vtable of an object against the vtable of a specific class, if these are equal then that ob- ject is in that class. Such comparisons produce facts in our system of the form x=null or vtable(x)=vtable(C). We can simply add axioms to our fact language like ⊢ x=null =⇒ x : C or ⊢ vtable(x)=vtable(C) =⇒ x : C. 6. Implementation Status The current implementation of the STARJIT compiler generates and maintains proof variables throughout its compilation process to enable safe implementation of certain optimizations in the presence of check elimination (to be described in a forthcoming paper). For their initially designed role in optimizations, proof variables did not require proof types: optimizations do not need to know the reason an optimization was safe, but only its safety dependences. As such, the current STARJIT representation is similar to that described in Section 2 with some of the extensions in Section 5. STARJIT implements all of the optimizations discussed in this paper as well as more described in [1]. We modified each opti- mization, if necessary, to correctly handle proof variables. Array bounds check elimination and operator strength reduction required the most significant modification, as described in Section 4. For partial inlining of virtual machine type checking functions, as de- scribed in Section 5, we updated the definition of proof variables to established that a variable has the checked type. We also modified method inlining to properly establish the type of inlined methods. For each parameter of a method, we added a proof variable that es- tablished that it had the correct type. When a method is compiled independently, that proof variable is trivially defined at the method entry (as parameter types to a method are guaranteed by the run- time environment). When the method is inlined, the corresponding proof variables must be defined by the calling method instead. As method call operations require proof variables for each parameter in our system, this information is readily available. Most optimiza- tions, however, did not require significant changes for the reasons outlined in this paper. An early version of a type verifier which inferred proof types it- self was implemented. This implementation was particularly help- ful in finding bugs within STARJIT, but was insufficient for com- plete verification of optimized code. In particular, the inference al- gorithm was insufficient for some more complicated optimization situations, such as the LFTR example (without proof type informa- tion) in Section 4. We are confident that extending the compiler to use precise proof types for proof variables will be straightforward, using the framework developed in this paper. 7. Related Work As far as we are aware, SafeTSA [24, 2] is the only other example of a type-safe SSA representation in the literature. The motivation of their work is rather different than ours. SafeTSA was designed as an alternative to Java bytecode, whereas our representation is de- signed to be a low-level intermediate language for a bytecode com- piler. SafeTSA can represent certain optimizations, such as CSE and limited check elimination, that Java bytecode does not. How- ever, in our classification in Section 2, SafeTSA is a refinement- style representation and, thus, cannot represent the effect of many of the low-level optimizations we discuss here. For example, it can- not represent the safety of check elimination based upon a previous branch or the construction of an unsafe memory address as illus- trated in Figure 7. On the other hand, we do not support their notion of referential security: the property that a program must be safe by construction. While most of the work on certified code focuses on the final machine code representation, there has been previous work on intermediate representations that allow verification of the memory safety of highly optimized machine level code. One of the major differences between the various approaches lies in the degree to which safety information is made explicit. On the side of less explicit information are the SpecialJ com- piler [8] and DTAL [26]. Both approaches record loop invariants, but not explicit safety dependences. This makes verification harder (all available invariants must be considered by the decision pro- cedure), interferes with more optimizations (such as loop peeling) than our approach, and makes removing dead invariants much more difficult (because invariants never have explicit uses). At the other end of the spectrum, there are other systems that not only represent dependences explicitly as we do, but also record ex- actly why the dependences imply safety for each instruction, using proofs, instead of relying on a decision procedure during checking, as in our system. The LTT system of Crary and Vanderwaart [10] and the TSCB system of Shao et al. [22], developed independently, both take this approach, albeit in the setting of a functional or mostly-functional language. Both systems are designed around the idea of incorporating a logic into a type theory, in order to combine the benefits of proof-carrying code [19] with the convenience of a type system. LTT and TSCB adopt the linear logical framework LLF and the Calculus of Inductive Constructions, respectively, as their proof languages. Incorporating a proof system also gives them more flexibility, as they can express a variety of properties within a single framework. The lack of explicit proofs in the representation forces us to use a decision procedure during typechecking. This limits us to decidable properties, and may be less suited for certified code applications where the added complexity of a decision procedure in the verifier may be undesirable. On the other hand, a system such as ours is much more suited to use in the internals of an optimizing compiler. For the limited use that we need proofs for—to verify the correctness of checks which are eliminated by a real optimizing compiler—we can get away with a vastly simpler system, one that imposes much less of a burden on the compiler than more syntactically heavy systems. Moreover, for applications of certified code, we believe that it should be possible to take optimized intermediate code in the style presented here and translate it, as part of code generation, to a more explicit form in the style of LTT or TSCB, thereby reaping the benefits of both approaches, perhaps by following the Special J model of using a proof generating theorem prover. However, this remains future work. Finally, our proof variables are also similar to the Jalapeño Java system’s condition registers as described in [6, 14]. Both are mech- anisms to represent control-flow information as abstract value de- pendences. Their usage, however, is more limited. Condition regis- ters are not used to express general safety information or to support verification of code. Instead, they are used by the compiler to model control flow between a check operation and all (rather than just po- tentially unsafe) instructions that follow it. Jalapeño uses condition registers to collapse control flow due to exceptions into a single ex- tended block and, in that block, to prevent instruction reordering that would violate control flow dependences. 8. Conclusions This paper has shown a typed low-level program representation that preserves memory safety dependences in highly-optimizing POPL ’06 Submission 11 2005/11/15
  • 12. type-preserving compilers. Our representation encodes safety de- pendences as first-class term-level proof variables that capture the essential memory-safety dependences in the program without artifi- cially constraining optimizations—previous approaches that piggy- back safety dependence on top of value dependence inhibit opti- mization opportunities. Our representation encodes proofs of mem- ory safety as dependent types associated with proof variables. Ex- perience implementing this representation in the STARJIT com- piler has demonstrated that a highly-optimizing Java JIT compiler can easily generate and maintain this representation in the pres- ence of aggressive SSA-based optimizations such as bounds check elimination, value numbering, strength reduction, linear function test replacement, and others. Using explicit proof values and proof types, modern optimizing compilers for type-safe languages can now generate provably safe yet low-level intermediate representa- tions without constraining optimizations. References [1] ADL-TABATABAI, A.-R., BHARADWAJ, J., CHEN, D.-Y., GHU- LOUM, A., MENON, V. S., MURPHY, B. R., SERRANO, M., AND SHPEISMAN, T. The StarJIT compiler: A dynamic compiler for man- aged runtime environments. Intel Technology Journal 7, 1 (February 2003). [2] AMME, W., DALTON, N., VON RONNE, J., AND FRANZ, M. SafeTSA: a type safe and referentially secure mobile-code repre- sentation based on static single assignment form. In Proceedings of the ACM SIGPLAN 2001 conference on Programming language de- sign and implementation (Snowbird, UT, USA, 2001), pp. 137–147. [3] BILARDI, G., AND PINGALI, K. Algorithms for computing the static single assignment form. J. ACM 50, 3 (2003), 375–425. [4] BODÍK, R., GUPTA, R., AND SARKAR, V. ABCD: Eliminating array bounds checks on demand. In Proceedings of the ACM SIGPLAN 2000 conference on Programming language design and implementation (Vancouver, British Columbia, Canada, 2000), pp. 321–333. [5] BRIGGS, P., COOPER, K. D., AND SIMPSON, L. T. Value numbering. Software—Practice and Experience 27, 6 (June 1996), 701–724. [6] CHAMBERS, C., PECHTCHANSKI, I., SARKAR, V., SERRANO, M. J., AND SRINIVASAN, H. Dependence analysis for Java. In Proceedings of the 12th International Workshop on Languages and Compilers for Parallel Computing (1999), vol. 1863 of Lecture Notes in Computer Science, pp. 35–52. [7] CHEN, J., AND TARDITI, D. A simple typed intermediate language for object-oriented languages. In Proceedings of the 32nd Annual ACM Symposium on Principles of Programming Languages (Long Beach, CA, USA, Jan. 2005), ACM Press, pp. 38–49. [8] COLBY, C., LEE, P., NECULA, G. C., BLAU, F., PLESKO, M., AND CLINE, K. A certifying compiler for Java. In PLDI ’00: Proceedings of the ACM SIGPLAN 2000 conference on Programming language design and implementation (New York, NY, USA, 2000), ACM Press, pp. 95–107. [9] COOPER, K. D., SIMPSON, L. T., AND VICK, C. A. Operator strength reduction. ACM Transactions on Programming Languages and Systems (TOPLAS) 23, 5 (September 2001), 603–625. [10] CRARY, K., AND VANDERWAART, J. An expressive, scalable type theory for certified code. In ACM SIGPLAN International Conference on Functional Programming (Pittsburgh, PA, 2002), pp. 191–205. [11] CYTRON, R., FERRANTE, J., ROSEN, B., WEGMAN, M., AND ZADECK, K. An efficient method of computing static single assignment form. In Proceedings of the Sixteenth Annual ACM Symposium on the Principles of Programming Languages (Austin, TX, Jan. 1989). [12] GLEW, N. An efficient class and object encoding. In Proceedings of the 15th ACM SIGPLAN conference on Object-oriented program- ming, systems, languages (Minneapolis, MN, USA, Oct. 2000), ACM Press, pp. 311–324. [13] GROSSMAN, D., AND MORRISETT, J. G. Scalable certification for typed assembly language. In TIC ’00: Selected papers from the Third International Workshop on Types in Compilation (London, UK, 2001), Springer-Verlag, pp. 117–146. [14] GUPTA, M., CHOI, J.-D., AND HIND, M. Optimizing Java programs in the presence of exceptions. In Proceedings of the 14th European Conference on Object-Oriented Programming - ECOOP ’00 (Lecture Notes in Computer Science, Vol. 1850) (June 2000), Springer-Verlag, pp. 422–446. [15] IGARASHI, A., PIERCE, B., AND WADLER, P. Featherweight Java: A minimal core calculus for Java and GJ. ACM Transactions on Programming Languages and Systems (TOPLAS) 23, 3 (May 2001), 396–560. First appeared in OOPSLA, 1999. [16] KNOOP, J., RÜTHING, O., AND STEFFEN, B. Lazy code motion. In Proceedings of the SIGPLAN ’92 Conference on Programming Language Design and Implementation (San Francisco, CA, June 1992). [17] MORRISETT, G., CRARY, K., GLEW, N., GROSSMAN, D., SAMUELS, R., SMITH, F., WALKER, D., WEIRICH, S., AND ZDANCEWIC, S. TALx86: A realistic typed assembly language. In Second ACM SIGPLAN Workshop on Compiler Support for System Software (Atlanta, Georgia, 1999), pp. 25–35. Published as INRIA Technical Report 0288, March, 1999. [18] MORRISETT, G., WALKER, D., CRARY, K., AND GLEW, N. From System F to typed assembly language. ACM Transactions on Programming Languages and Systems (TOPLAS) 21, 3 (May 1999), 528—569. [19] NECULA, G. Proof-carrying code. In POPL1997 (New York, New York, January 1997), ACM Press, pp. 106–119. [20] NECULA, G. C., AND LEE, P. The design and implementation of a certifying compiler. In PLDI ’98: Proceedings of the ACM SIGPLAN 1998 conference on Programming language design and implementation (New York, NY, USA, 1998), ACM Press, pp. 333– 344. [21] PUGH, W. The Omega test: A fast and practical integer programming algorithm for dependence analysis. In Proceedings of Supercomput- ing ’91 (Albuquerque, NM, Nov. 1991). [22] SHAO, Z., SAHA, B., TRIFONOV, V., AND PAPASPYROU, N. A type system for certified binaries. In Proceedings of the 29th Annual ACM Symposium on Principles of Programming Languages (January 2002), ACM Press, pp. 216–232. [23] VANDERWAART, J. C., DREYER, D. R., PETERSEN, L., CRARY, K., AND HARPER, R. Typed compilation of recursive datatypes. In Proceedings of the TLDI 2003: ACM SIGPLAN International Workshop on Types in Language Design and Implementation (New Orleans, LA, January 2003), pp. 98–108. [24] VON RONNE, J., FRANZ, M., DALTON, N., AND AMME, W. Compile time elimination of null- and bounds-checks. In 3rd Workshop on Feedback-Directed and Dynamic Optimization (FDDO- 3) (December 2000). [25] WALKER, D., CRARY, K., AND MORISETT, G. Typed memory man- agement via static capabilities. ACM Transactions on Programming Languages and Systems (TOPLAS) 22, 4 (July 2000), 701–771. [26] XI, H., AND HARPER, R. Dependently typed assembly language. In International Conference on Functional Programming (September 2001), pp. 169–180. POPL ’06 Submission 12 2005/11/15
  • 13. Γ ⊢P v : τ in L at du L ⊢P e is v at du L ⊢P F at du x ∈ dom(L) x ∈ inscopeP (du) Γ ⊢P L(x) : S(x) in L at du Γ ⊢P i : int in L at du Γ ⊢P v : τ in L at du Γ ⊢P hvi : array(τ) in L at du Γ ⊢P v : τ in L at du Γ ⊢P hvi@i : ptr?hτi in L at du L ⊢P F at du Γ ⊢P true : pf(F ) in L at du Γ ⊢P v : τ1 in L at du Γ ⊢ τ1 ≤ τ2 Γ ⊢P v : τ2 in L at du x ∈ dom(L) x ∈ inscopeP (du) L ⊢P x is L(x) at du L ⊢P x is hv0, . . . , vn−1i at du L ⊢P len(x) is n at du L ⊢P i is i at du L ⊢P e1 is i1 at du L ⊢P e2 is i2 at du L ⊢P e1bope2 is i1 bop i2 at du L ⊢P e1 is i1 at du L ⊢P e2 is v@i2 at du L ⊢P e1bope2 is v@(i1 bop i2) at du L ⊢P e1 is v@i1 at du L ⊢P e2 is i2 at du L ⊢P e1bope2 is v@(i1 bop i2) at du L ⊢P x is hvi at du L ⊢P e is i at du L ⊢P x@e is hvi@i at du L ⊢P e1 is hv0, . . . , vni@i1 at du L ⊢P e2 is hv0, . . . , vni@i2 at du i1 rop i2 L ⊢P e1rope2 at du L ⊢P e1 is i1 at du L ⊢P e2 is i2 at du i1 rop i2 L ⊢P e1rope2 at du L ⊢P F1 at du L ⊢P F2 at du L ⊢P F1 ∧ F2 at du Γ ⊢P L : Γ′ @V ⊢ S ∀x ∈ V : Γ ⊢P L(x) : Γ′ (x) in L at defP (x) Γ ⊢P L : Γ′ @V ⊢ P vt(P) = Γ Γ ⊢P L : Γ@dfndAtP (pc, n) n is an in-edge number for b where pc = b.i pc ∈ pcs(P) ∀x ∈ dfndAtP (pc, n) if deffactP (x) = F then L ⊢P F at pc ⊢ (P, L, n, pc) Figure 20. Typing for States, Environments, Values, and Facts A. Appendix: Proof of Type Safety A.1 Preliminaries ASSUMPTION 1 (Logical Soundness). If L ⊢P F1 at du and ⊢ F1 =⇒ F2 then L ⊢P F2 at du. Note that if domP (n1, n2) and domP (n2, n1) then n1 = n2. Hence strict dominance is asymetric. Thus dominance and strict dominance induce a partial order that we can think of as a dominance tree rooted at (−1, 0) and (−1, 0).0 respectively. Let inscopeP (du) = {x | sdomP (defP (x), du)}. The latter set is the variables we know are defined at the beginning of an instruction (if du is the program counter). However, at the beginning of the phi instructions we also need variables in scope on the incoming edge to be defined. Therefore, we define dfndAtP (b.0, e) to be inscopeP (e.1), dfndAtP (b.(i + 1), e) to be inscopeP (b.(i + 1)), and dfndAtP (b.i, n) to be dfndAtP (b.i, e) where e is the n-th incoming edge to b. The typing for states, environments, values, and facts appears in Figure 20. A.2 Auxiliary lemmas LEMMA 1. If L1(x) = L2(x) for all x ∈ inscopeP (du) and sdomP (du, du′ ) or du = du′ then: • If Γ ⊢P v : τ in L1 at du then Γ ⊢P v : τ in L2 at du′ . • If L1 ⊢P F at du then L2 ⊢P F at du′ . Proof: The proof is by induction on the typing derivation. In the case of the singleton value rule and the rule for the value of an expression that is a variable, the variable in question has to be in scope for du, so L1 and L2 agree on its value and the variable is in scope for du′ . COROLLARY 1 (Environment weakening). • If L ⊢P e is ve at du and x / ∈ inscopeP (du) and then L{x := v} ⊢P e is ve at du • If L ⊢P F at du and x / ∈ inscopeP (du) then L{x := v} ⊢P F at du Proof: Follows immediately from lemma 1. POPL ’06 Submission 13 2005/11/15
  • 14. LEMMA 2. If Γ ⊢P L1 : Γ@inscopeP (du), x / ∈ inscopeP (du), L2 = L1{x := v} and Γ ⊢P v : Γ(x) in L2 at defP (x) then Γ ⊢P L2 : Γ@inscopeP (du) ∪ {x}. Proof: The result follows by the typing rule if Γ ⊢P L2(x) : Γ(x) in L2 at defP (x) for x ∈ inscopeP (du) ∪ {x}. For x in the latter, the judgement holds by hypothesis. For x in the former, note that inscopeP (defP (x)) ⊆ inscopeP (du), so L1(y) = L2(y) for all y ∈ inscopeP (defP (x)) and clearly L1(x) = L2(x). Thus the judgement holds by hypothesis and Lemma 1. Note that subtyping is reflexive and transitive. LEMMA 3 (Subtyping Inversion). • If Γ ⊢ τ ≤ S(x) then τ = S(y) for some y. • If Γ ⊢ S(x) ≤ τ then either τ = S(x) or Γ ⊢ Γ(x) ≤ τ. • If Γ ⊢ array(τ1) ≤ array(τ2) then Γ ⊢ τ1 ≤ τ2. • If Γ ⊢ ptr?hτ1i ≤ ptr?hτ2i then Γ ⊢ τ1 ≤ τ2. • If Γ ⊢ pf(F1) ≤ pf(F2) then ⊢ F1 =⇒ F2. • The following are not derivable: Γ ⊢ int ≤ array(τ), Γ ⊢ int ≤ ptr?hτi, Γ ⊢ int ≤ S(x), Γ ⊢ int ≤ pf(F ), Γ ⊢ array(τ) ≤ int, Γ ⊢ array(τ) ≤ ptr?hτ′ i, Γ ⊢ array(τ) ≤ S(x), Γ ⊢ array(τ) ≤ pf(F ), Γ ⊢ ptr?hτi ≤ int, Γ ⊢ ptr?hτi ≤ array(τ′ ), Γ ⊢ ptr?hτi ≤ S(x), Γ ⊢ ptr?hτi ≤ pf(F ), Γ ⊢ pf(F ) ≤ int, Γ ⊢ pf(F ) ≤ array(τ), Γ ⊢ pf(F ) ≤ ptr?hτi, and Γ ⊢ pf(F ) ≤ S(x). Proof: The proof is by induction on the derivation of the subtyping judgement. The result is clear for all the rules except the transitivity rule. There is some intermediate type σ that is a supertype of the left type we are considering and a subtype of the right type we are considering. For the first item, σ is a subtype of S(x) so by the induction hypothesis, σ = S(z) for some z. Since σ is a supertype of τ, by the induction hypothesis, τ = S(y) for some y, as required. For the second item, σ is a supertype of S(x), so by the induction hypothesis eitehr σ = S(x) or Γ ⊢ Γ(x) ≤ σ. In the first case, the result follows by the induction hypothesis on the other judgement. In the second case, the result follows by transitivity of subtyping. For the third item, since σ is a supertype of an array type, by the induction hypothesis, σ must be an array type, say array(σ′ ). Then by the induction hypothesis for both judgements, Γ ⊢ τ1 ≤ σ′ and Γ ⊢ σ′ ≤ τ2. By transitivity of subtyping, Γ ⊢ τ1 ≤ τ2 as required. The fourth and fifth items are similar (the fifth requires transitivity of implication in the logic). For the sixth item, consider the cases. If the left type is int, an array type, a pointer type, or a proof type, then by the induction hypothesis σ must be of the same form, so by the induction hypothesis again, the right type must have the same form. These are all the cases we need to consider for the sixth item. LEMMA 4 (Canonical Forms). If Γ ⊢P L : Γ@V , x ∈ V , and inscopeP (y) ⊆ V for y ∈ V then: • If Γ ⊢ S(x) ≤ S(x′ ) and x′ ∈ V then L(x) = L(x′ ). • If Γ ⊢ Γ(x) ≤ int then L(x) is an integer. • If Γ ⊢ Γ(x) ≤ array(τ) then L(x) has the form hvi and Γ ⊢P v : τ in L at defP (x). • If Γ ⊢ Γ(x) ≤ ptr?hτi then L(x) has the form hvi@i and Γ ⊢P v : τ in L at defP (x). • If Γ ⊢ Γ(x) ≤ pf(F) then L ⊢P F at defP (x). Proof: For the first item, if the hypothesis holds then by Subtype Inversion either S(x) = S(x′ ) or Γ ⊢ Γ(x) ≤ S(x′ ). For the former, x = x′ and the conclusion therefore holds. Thus we need only show the first item for the stronger hypothesis that Γ ⊢ Γ(x) ≤ S(x′ ) and x′ ∈ V . The proof is by induction on the depth of x in the dominance tree. Since x ∈ V , by the typing rule, Γ ⊢P L(x) : Γ(x) in L at defP (x). This judgement can be derived by a non-subsumption rule followed by zero or more uses of the subsumption rule. Since subtyping is reflexive and transitive, the zero or multiple uses of subsumption can be transformed into exactly one use. Consider the non-subsumption rule used: Singleton Value Rule: In this case, L(x) = L(x′′ ), x′′ ∈ dom(L), x′′ ∈ inscopeP (defP (x)), and Γ ⊢ S(x′′ ) ≤ Γ(x). Since x′′ ∈ inscopeP (defP (x)), x′′ ∈ V and x′′ is less deep in the dominance tree than x. By the induction hypothesis, the result holds for x = x′′ , we just need to show that it holds for x. If the hypothesis of the first item holds (Γ ⊢ Γ(x) ≤ S(x′ ) and x′ ∈ V ), then by transitivity Γ ⊢ S(x′′ ) ≤ S(x′ ), so by the induction hypothesis, L(x′′ ) = L(x′ ). Thus L(x) = L(x′ ) as required. If the hypothesis of the third item holds (Γ ⊢ Γ(x) ≤ array(τ)), then by Subtyping Inversion on Γ ⊢ S(x′′ ) ≤ Γ(x) either S(x′′ ) = Γ(x) or Γ ⊢ Γ(x′′ ) ≤ Γ(x). For the former, we have Γ ⊢ S(x′′ ) ≤ array(τ), so by Subtyping Inversion Γ ⊢ Γ(x′′ ) ≤ array(τ). For the latter, the last judgement holds by transitivity. Then by the induction hypothesis L(x′′ ) has the form hvi and Γ ⊢P v : τ in L at defP (x′′ ). By Lemma 1, Γ ⊢P v : τ in L at defP (x), as required. The cases for the second and fourth items are similar to the case for the third item. If the hypothesis for the fifth item holds then by similar reasoning to the third item, Γ ⊢ Γ(x′′ ) ≤ pf(F). By the induction hypothesis, L ⊢P F at defP (x′′ ). By Lemma 1, L ⊢P F at defP (x), as required. Integer Rule: In this case, L(x) = i for some i and Γ ⊢ int ≤ Γ(x). The second item clearly holds. If the hypothesis of the other items held then by transitivity of subtyping, int would be a subtype of a singleton, array, pointer, or proof type, which is not possible by Subtyping Inversion. Array Rule: In this case, L(x) = hvi, Γ ⊢P v : τ in L at defP (x), and Γ ⊢ array(τ) ≤ Γ(x). If the hypothesis of the third item, namely Γ ⊢ Γ(x) ≤ array(σ), holds then by transitivity of subtyping and Subtyping Inversion, Γ ⊢ τ ≤ σ. Then by subsumption Γ ⊢P v : σ in L at defP (x) as required by the conclusion of item three. If the hypothesis of the other items held then by transitivity of subtyping, array(τ) would be a subtype of a singleton, integer, pointer, or proof type, which is not possible by Subtyping Inversion. Pointer Rule: In this case, L(x) = hvi@i, Γ ⊢P v : τ in L at defP (x), and Γ ⊢ ptr?hτi ≤ Γ(x). If the hypothesis of the fourth item, namely Γ ⊢ Γ(x) ≤ ptr?hσi, holds then by transitivity of subtyping and Subtyping Inversion, Γ ⊢ τ ≤ σ. Then by subsumption POPL ’06 Submission 14 2005/11/15
  • 15. Γ ⊢P v : σ in L at defP (x) as required by the conclusion of item four. If the hypothesis of the other items held then by transitivity of subtyping, ptr?hτi would be a subtype of a singleton, integer, array, or proof type, which is not possible by Subtyping Inversion. Proof Rule: In this case, L ⊢P F′ at defP (x) and Γ ⊢ pf(F′ ) ≤ Γ(x). If the hypothesis of the fifth item, name Γ ⊢ Γ(x) ≤ pf(F ), held, then by transitivity of subtyping and Subtyping Inversion, ⊢ F′ =⇒ F. Then by Logical Soundness, L ⊢P F at defP (x), as required by the conclusion to item five. If the hypothesis of the other items held then by transitivity of subtyping, pf(F ′) would be a subtype of a singleton, integer, array, or pointer type, which is not possible by Subtyping Inversion. LEMMA 5. For any b a block number for P and n an incoming edge number to b, inscopeP (b.i) ⊆ dfndAtP (b.i, n). Proof: If i > 0 then the result holds by definition. Otherwise let (b′ , b) be the n-th incoming edge to b. Then dfndAtP (b.i, n) = inscopeP ((b′ , b).1). Let N be the parent of b in the dominance tree for P. If N is not equal to or an ancestor of (b′ , b) then there exists a path from (−1, 0) to (b′ , b) that does not include N. We can extend this path with the edge from (b′ , b) to b to obtain a path from (−1, 0) to b that does not include N contradicting the fact that N is b’s parent in the dominance tree. Let x ∈ inscopeP (b.0) then sdomP (defP (x), b.0). Let defP (x) = b′′ .i then domP (b′′ , N), so domP (b′′ , (b′ , b)). Since (b′ , b).1 does not define any variables, sdomP (defP (x), (b′ , b).1) and x ∈ inscopeP ((b′ , b).1), as required. LEMMA 6 (Canonical Forms 2). If Γ ⊢P L : Γ@inscopeP (du) and Γ ⊢P v : τ in L at du then: • If τ = int then v = i. • If τ = array(σ) then v = hvi and Γ ⊢P v : σ in L at du. • If τ = ptr?hσi then v = hvi@i and Γ ⊢P v : σ in L at du. • If τ = S(x) then v = L(x) and x ∈ inscopeP (du). • If τ = pf(F ) then v = true and L ⊢P F at du. Proof: The proof is by induction on the depth of du is the dominance tree. The judgement Γ ⊢P v : τ in L at du can only be derived by a nonsubsumption rule following by zero or more uses of the subsumption rule. Since subtyping is reflexive and transitive, we can turn these uses of subsumption into exactly one use. Consider the nonsubsumption rule used: Singleton Rule: In this case v = L(x), x ∈ inscopeP (du), and Γ ⊢ S(x) ≤ τ. If τ = S(x) then the result holds. Otherwise, by Subtyping Inversion Γ ⊢ Γ(x) ≤ τ. By hypothesis, Γ ⊢P v : Γ(x) in L at defP (x). By subsumption, Γ ⊢P v : τ in L at defP (x). Since x ∈ inscopeP (du), defP (x) ∈ inscopeP (du), so sdomP (defP (x), du). Thus defP (x) is higher in the dominance tree than du. The result follows by the induction hypothesis. Integer Rule: In this case v is some i and by Subtyping Inversion τ must be int, as required. Array Rule: In this case v is hvi, Γ ⊢P v : τ1 in L at du, and Γ ⊢ array(τ1) ≤ τ. By Subtyping Inversion τ must be array(τ2) and Γ ⊢ τ1 ≤ τ2. By subsumption, Γ ⊢P v : τ2 in L at du, as required. Pointer Rule: In this case v is ARRAY v@i, Γ ⊢P v : τ1 in L at du, and Γ ⊢ ptr?hτ1i ≤ τ. By Subtyping Inversion τ must be ptr?hτ2i and Γ ⊢ τ1 ≤ τ2. By subsumption, Γ ⊢P v : τ2 in L at du, as required. Proof Rule: In this case v is true, L ⊢P F1 at du, and Γ ⊢ pf(F1) ≤ τ. By Subtyping Inversion τ must be pf(F2) and ⊢ F1 =⇒ F2. By Logical Soundness L ⊢P F2 at du, as required. LEMMA 7. If Γ ⊢P L : Γ@inscopeP (du) then: • If ⊢P τ at defP (x) and Γ ⊢P v : τ{x1 := x2} in L at du then Γ ⊢P v : τ in L{x1 := L(x2)} at defP (x). • If ⊢P F at defP (x) and L ⊢P F{x1 := x2} at du then L{x1 := L(x2)} ⊢P F at defP (x). • If ⊢P e at defP (x) and L ⊢P e{x1 := x2} is v at du then L{x1 := L(x2)} ⊢P e is v at defP (x) Proof: Let ρ = x1 := x2 and L′ = L{x1 := L(x2)}. The proof is by induction of the structure of τ, F, or e. Consider the different forms that τ, F, or e could take: τ = int: In this case, τ = τ{ρ}, so the hypothesis and Canonical Forms 2 imply that v = i. The conclusion then follows by the integer rule. τ = array(σ): In this case, τ{ρ} = array(σ{ρ}), so by hypothesis and Canonical Forms 2, v = hvi and Γ ⊢P v : σ{ρ} in L at du, by the induction hypothesis, Γ ⊢P v : σ in L′ at defP (x), so by the array rule the conclusion holds. τ = ptr?hσi: In this case, τ{ρ} = ptr?hσ{ρ}i, so by hypothesis and Canonical Forms 2, v = hvi@i and Γ ⊢P v : σ{ρ} in L at du. By the induction hypothesis, Γ ⊢P v : σ in L′ at defP (x), so by the pointer rule the conclusion holds. τ = S(z): Let y be ρ(z). Then τ{ρ} = S(y) and by hypothesis and Canonical Forms 2, v = L(y). Since ⊢P τ at defP (x), z ∈ inscopeP (defP (x)). Clearly L′ (z) = L(y) and z ∈ dom(L′ ). Thus by the singleton vlaue rule, Γ ⊢P v : S(z) in L′ at defP (x), as required. τ = pf(F ): In this case, τ{ρ} = pf(F {ρ}), so by hypothesis and Canonical Forms 2, v = true and L ⊢P F{ρ} at du. By the induction hypothesis, L′ ⊢P F at defP (x), and the conclusion holds by the proof value rule. F = e1 rop e2: In this case, F{ρ} = e1{ρ} rop e2{ρ}. Since the hypothesis can be derived by only two rules, it must be the case that L ⊢P e1{ρ} is v1 at du, L ⊢P e2{ρ} is v2 at du, v1 and v2 have the forms i1 and i2 or the forms hvi@i1 and hvi@i2, and i1 rop i2. POPL ’06 Submission 15 2005/11/15
  • 16. By the induction hypothesis, L′ ⊢P e1 is v1 at defP (x) and L′ ⊢P e2 is v2 at defP (x). The conclusion follows by applying the same rule. F = F1 ∧ F2: In this case, F{ρ} = F1{ρ} ∧ F2{ρ}. Since the hypothesis can be derived in only one way, L ⊢P F1{ρ} at du and L ⊢P F2{ρ} at du. By the induction hypothesis, L′ ⊢P F1 at defP (x) and L′ ⊢P F2 at defP (x). The conclusion follows the the and rule. e = i: In this case the conclusion follows by the integer rule. e = z: Let y be ρ(z). Then e{ρ} = y. The hypothesis can be derived in only one way, so v = L(y). Clearly, L′ (z) = L(y) and z ∈ dom(L′ ). Since ⊢P e at defP (x), z ∈ inscopeP (defP (x)). Thus by the expression variable rule, L′ ⊢P z is v at defP (x), as required. e = len(z): Let y be ρ(z). Then e{ρ} = len(y). The hypothesis can be derived in only one way, so L ⊢P y is hv0, . . . , vn−1i at du and v = n. By the induction hypothesis, L′ ⊢P z is hv0, . . . , vn−1i at defP (x). The conclusion follows by the length rule. e = e1 bop e2: In this case, e{ρ} = e1{ρ} bop e2{ρ}. Since the hypothesis can be derived in only one way, L ⊢P e1{ρ} is i1 at du, L ⊢P e2{ρ} is i2 at du, and v = i1 bop i2. By the induction hypothesis, L′ ⊢P e1 is i1 at defP (x) and L′ ⊢P e2 is i2 at defP (x). The conclusion follows by the binary operation rule. e = z@e′ : Let y be ρ(z). Then e{ρ} = y@e′ {ρ}. The hypothesis can be derived in only one way, so L ⊢P y is hvi at du, L ⊢P e′ {ρ} is i at du, and v = hvi@i. By the induction hypothesis, L′ ⊢P x is hvi at defP (x) and L′ ⊢P e′ is i at defP (x). The conclusion follows by the pointer rule. A.3 Preservation LEMMA 8. If ⊢ P then ⊢ (P, ∅, 0, 0.0). Proof: Straightforward given that dfndAtP (0.0, 0) = ∅. LEMMA 9 (Preservation). If ⊢ S1 and S1 7→ S2 then ⊢ S2. Proof: Assume that ⊢ (P, L1, e1, b.i) and (P, L1, e1, b.i) 7→ (P, L2, e2, pc). Let Γ = vt(P). By the typing rule for programs: 1. ⊢ P 2. Γ ⊢P L1 : Γ@dfndAtP (b.i, e1) 3. e1 is a valid in-edge number for b 4. b.i ∈ pcs(P) 5. ∀x ∈ dfndAtP (b.i, e1) if deffactP (x) = F then L1 ⊢P F at b.i If P(b.i) = ι or P(b.i) = p then pc = b.(i + 1) ∈ pcs(P) and e2 = e1 so e2 is a valid in-edge number for pc’s block. We will show that validity of pc and e2 for transfers in the respective rules below. Thus it remains to show that: 1. Γ ⊢P L2 : Γ@dfndAtP (pc, e2) 2. ∀x ∈ dfndAtP (pc, e2) if deffactP (x) = F then L2 ⊢P F at pc For all instructions for which deffactP (x) is not defined, note that (2) follows immediately by lemma 1, since the set of defined facts remains unchanged. For instructions for which deffactP (x) = F, it suffices to show that L2 ⊢P F at pc. The proof proceeds by case analysis on the reduction rule. Phi rule: In this case, P(b.i) = p, p[e1] = x1 := x2, and L2 = L1{x1 := L1(x2)}. By the definitions, dfndAtP (pc, e2) = inscopeP (pc) = inscopeP (b.i) ∪ {x1}. By Lemma 5, inscopeP (b.i) ⊆ dfndAtP (b.i, e1). Clearly by the typing rules and Γ ⊢P L1 : Γ@dfndAtP (b.i, e1), Γ ⊢P L1 : Γ@inscopeP (b.i). So by Lemma 2, we just need to show that Γ ⊢P L1(x2) : Γ(x1) in L2 at defP (x1) (note that φ instructions define no facts). Let (b′ , b) be the e1’th incoming edge to b. Since the phi instructions are uses of x2 at (b′ , b).1, defP (x2) ⊆ inscopeP ((b′ , b).1) = dfndAtP (b.i, e1). By Env Typing, x2 ⊆ dom(L1). Thus by the singleton typing rule, Γ ⊢P L1(x2) : S(x2) in L1 at (b′ , b).1. By the typing rules for phi-instructions, Γ ⊢ S(x2) ≤ Γ(x1){ρ} where ρ is x1 := x2. Thus by subsumption, Γ ⊢P L1(x2) : Γ(x1){ρ} in L1 at (b′ , b).1. By the typing rules, ⊢P Γ(x1) at defP (x1). So by Lemma 7, Γ ⊢P L1(x2) : Γ(x1) in L2 at defP (x1). Constant rule: In this case, P(b.i) = x := i and L2 = L1{x := i}. Also note that deffactP (x) = (x = i). By expansion of the definitions: defP (x) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x} x / ∈ dfndAtP (b.i, e1) First we must show that: L2 ⊢P x = i at pc By the environment rule (since L2(x) = i): L2 ⊢P x is i at pc By the integer rule: L2 ⊢P i is i at pc So by the comparison rule: L2 ⊢P x = i at pc POPL ’06 Submission 16 2005/11/15
  • 17. By Lemma 2, it suffices to show that Γ ⊢P i : Γ(x) in L2 at b.i. By assumption: Γ ⊢P x : τ := i So by inversion: Γ ⊢ int ≤ Γ(x) So by construction using the integer rule and subsumption: Γ ⊢P i : Γ(x) in L2 at b.i Copy rule: In this case, P(b.i) = x1 := x2 and L2 = L1{x1 := L1(x2)}. By expansion of the definitions, defP (x1) = b.i, dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1}, and x1 / ∈ dfndAtP (b.i, e1). By Lemma 2, we need to show that Γ ⊢P L1(x2) : Γ(x1) in L2 at b.i. The typing rule for this instruction include Γ ⊢ S(x2) ≤ Γ(x1). Since this instruction is a use of x2 and the in-scope property, x2 ∈ dfndAtP (b.i, e1), thus x1 6= x2, x2 ∈ dom(L1), L2(x2) = L1(x2), and x2 ∈ inscopeP (b.i). By the singleton typing rule and subsumption, Γ ⊢P L1(x2) : Γ(x1) in L2 at b.i, as required. New array (i ≥ 0) In this case P(b.i) = x1 : τ := newarray(x2, x3) and L2 = L1{x1 := v1}, where L1(x2) = n, L1(x3) = v3, v1 = hv3, . . . , v3 | {z } n i. By expansion of the definitions: defP (x1) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x} x2, x3 ∈ dfndAtP (b.i, e1) x1 / ∈ dfndAtP (b.i, e1) By Lemma 2, it suffices to show that Γ ⊢P v1 : Γ(x1) in L2 at b.i. By assumption: Γ ⊢P x1 : τ := newarray(x2, x3) By inversion of Γ ⊢P x1 : τ := newarray(x2, x3): Γ ⊢ array(Γ(x3)) ≤ Γ(x1) By assumption (since x3 ∈ dfndAtP (b.i, e1)): Γ ⊢P v3 : Γ(x3) in L2 at b.i So by construction, using the newarray rule and subsumption: Γ ⊢P v1 : Γ(x1) in L2 at b.i New array (i < 0) The proof proceeds exactly as in the previous case, except that there is no proof obligation for v3, and hence the construction from the newarray rule follows immediately. Array length rule In this case, P(b.i) = x1 : τ := len(x2) and L2 = L1{x1 := n} where L1(x2) = hv0, . . . , vn−1i. Also note that deffactP (x1) = (x = len(x2)) By expansion of the definitions: defP (x1) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x} x2 ∈ dfndAtP (b.i, e1) x1 / ∈ dfndAtP (b.i, e1) First we must show that: L2 ⊢P x1 = len(x2) at pc By the environment rule (since L2(x1) = n, and L2(x2) = hv0, . . . , vn−1i): L2 ⊢P x1 is n at pc L2 ⊢P x2 is hv0, . . . , vn−1i at pc So by the length rule: L2 ⊢P len(x2) is n at pc So by the comparison rule: L2 ⊢P x1 = len(x2) at pc By Lemma 2, it suffices to show that Γ ⊢P n : Γ(x1) in L2 at b.i. By assumption: Γ ⊢P x1 : τ := len(x2) By inversion: Γ ⊢ int ≤ Γ(x1) So the result holds by construction using the integer rule and subsumption. Pointer base rule In this case, P(b.i) = x1 : τ := base(x2) and L2 = L1{x2 := v@0}, where L1(x2) = v, v = hv′i. Note that deffactP (x1) = (x1 = x2@0) By expansion of the definitions: defP (x1) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1} x2 ∈ dfndAtP (b.i, e1) x1 / ∈ dfndAtP (b.i, e1) POPL ’06 Submission 17 2005/11/15
  • 18. First we must show that: L2 ⊢P x1 = x2@0 at pc By the environment rule (since L2(x1) = v@0, and L2(x2) = v): L2 ⊢P x1 is v@0 at pc L2 ⊢P x2 is v at pc So by the managed pointer rule: L2 ⊢P x2@0 is v@0 at pc So by the comparison rule: L2 ⊢P x1 = x2@0 at pc By Lemma 2, it suffices to show that Γ ⊢P v@0 : Γ(x1) in L2 at b.i. By assumption: Γ ⊢P x1 : τ := base(x2) By inversion: Γ ⊢ Γ(x2) ≤ array(τ2) Γ ⊢ ptr?hτ2i ≤ Γ(x1) So by Canonical Forms: Γ ⊢P v′ : τ2 in L1 at defP (x2) Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and x1 / ∈ inscopeP (defP (x1)). So by lemma 1: Γ ⊢P v′ : τ2 in L2 at b.i So the result holds by construction using the managed pointer rule and subsumption. Binary op rule (int) In this case, P(b.i) = x1 : τ := x2 bop x3 and L2 = L1{x1 := i2 bop i3}: where L1(x2) = i2, L1(x3) = i3. Note that deffactP (x1) = (x1 = x2 bop x3). By expansion of the definitions: defP (x1) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1} x2, x3 ∈ dfndAtP (b.i, e1) x1 / ∈ dfndAtP (b.i, e1) First we must show that: L2 ⊢P x1 = x2 bop x3 at pc By the environment rule (since L2(x1) = i2 bop i3, L2(x2) = i2, and L2(x3) = i3): L2 ⊢P x1 is i2 bop i3 at pc L2 ⊢P x2 is i2 at pc L2 ⊢P x3 is i3 at pc So by the integer arithmetic rule: L2 ⊢P x2 bop x3 is i2 bop i3 at pc So by the comparison rule: L2 ⊢P x1 = x2 bop x3 at pc By Lemma 2, it suffices to show that Γ ⊢P i2 bop i3 : Γ(x1) in L2 at b.i. By assumption: Γ ⊢P x1 : τ := x2 bop x3 So by inversion: Γ ⊢ int ≤ Γ(x1) So the result holds by construction using the integer rule and subsumption. Binary op rule (pointer) In this case, P(b.i) = x1 : τ := x2 bop x3 and L2 = L1{x1 := v@i2 bop i3}: where L1(x2) = v@i2, L1(x3) = i3. Note that deffactP (x1) = (x1 = x2 bop x3). By expansion of the definitions: defP (x1) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1} x2, x3 ∈ dfndAtP (b.i, e1) x1 / ∈ dfndAtP (b.i, e1) First we must show that: L2 ⊢P x1 = x2 bop x3 at pc By the environment rule (since L2(x1) = v@(i2 bop i3), L2(x2) = v@i2, and L2(x3) = i3): L2 ⊢P x1 is v@(i2 bop i3) at pc L2 ⊢P x2 is v@i2 at pc L2 ⊢P x3 is i3 at pc So by the pointer arithmetic rule: L2 ⊢P x2 bop x3 is v@(i2 bop i3) at pc So by the pointer comparison rule: L2 ⊢P x1 = x2 bop x3 at pc By Lemma 2, it suffices to show that Γ ⊢P v@i2 bop i3 : Γ(x1) in L2 at b.i. POPL ’06 Submission 18 2005/11/15
  • 19. By assumption: Γ ⊢P x1 : τ := x2 bop x3 So by inversion: Γ ⊢ Γ(x2) ≤ ptr?hτ2i Γ ⊢ Γ(x3) ≤ int Γ ⊢ ptr?hτ2i ≤ Γ(x1) So by Canonical Forms: Γ ⊢P v : τ2 in L1 at defP (x2) Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and x1 / ∈ inscopeP (defP (x1)). So by lemma 1: Γ ⊢P v : τ2 in L2 at b.i So the result holds by construction using the managed pointer rule and subsumption. Load rule In this case, P(b.i) = x1 : τ := ld(x2) [x3] and L2 = L1{x1 := vi}: where L1(x2) = hv0, . . . , vni@i, 0 ≤ i ≤ n. By expansion of the definitions: defP (x1) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1} x2, x3 ∈ dfndAtP (b.i, e1) x1 / ∈ dfndAtP (b.i, e1) By Lemma 2, it suffices to show that Γ ⊢P vi : Γ(x1) in L2 at b.i. By assumption: Γ ⊢P x1 : τ := ld(x2) [x3] So by inversion: Γ ⊢ Γ(x2) ≤ ptr?hτ2i Γ ⊢ Γ(x3) ≤ pf(x@0≤x2∧x2<x@len(x)) Γ ⊢ τ2 ≤ Γ(x1) So by Canonical Forms: Γ ⊢P v : τ2 in L1 at defP (x2) Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and that x1 / ∈ inscopeP (defP (x1)). So by lemma 1: Γ ⊢P v : τ2 in L2 at b.i So in particular: Γ ⊢P vi : τ2 in L2 at b.i So the result holds by subsumption. Proof Fact In this case, P(b.i) = x1 : τ := pffact(x2) and L2 = L1{x1 := true}. By expansion of the definitions: defP (x1) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x1} x2 ∈ dfndAtP (b.i, e1) x1 / ∈ dfndAtP (b.i, e1) By Lemma 2, it suffices to show that Γ ⊢P true : Γ(x1) in L2 at b.i. By assumption: Γ ⊢P x1 : τ := pffact(x2) By inversion: Γ ⊢ pf(deffactP (x2)) ≤ Γ(x1) By assumption, L is consistent. Therefore, since x2 ∈ inscopeP (b.i), L1 ⊢P deffactP (x2) at defP (x2). Note that b.i is a use of x2, so by the in-scope property, sdomP (defP (x2), b.i), and x1 / ∈ inscopeP (b.i). So by lemma 1: L2 ⊢P deffactP (x2) at b.i By the true rule: Γ ⊢P true : pf(deffactP (x2)) in L2 at b.i By subsumption: Γ ⊢P true : Γ(x1) in L2 at b.i Proof conjunction In this case, P(b.i) = x : τ := pfand(y1, . . . , yn) and L2 = L1{x := true}. By expansion of the definitions: defP (x) = b.i dfndAtP (pc, e2) = dfndAtP (b.i, e1) ∪ {x} y1, . . . , yn ∈ dfndAtP (b.i, e1) x / ∈ dfndAtP (b.i, e1) By Lemma 2, it suffices to show that Γ ⊢P true : Γ(x) in L2 at b.i. By assumption: Γ ⊢P x : τ := pfand(y1, . . . , yn) POPL ’06 Submission 19 2005/11/15
  • 20. By inversion: Γ ⊢ Γ(y1) ≤ pf(F1) · · · Γ ⊢ Γ(yn) ≤ pf(Fn) Γ ⊢ pf(F1∧···∧Fn) ≤ Γ(x) By Canonical Forms: L1 ⊢P F1 at defP (y1) · · · L1 ⊢P Fn at defP (yn) Note that b.i is a use of x1 through yn, so by the in-scope property, sdomP (defP (y1), b.i) through sdomP (defP (yn), b.i). So by lemma 1: L1 ⊢P F1 at b.i · · · L1 ⊢P Fn at b.i By the conjunction rule: L1 ⊢P F1 ∧ · · · ∧ Fn at b.i Note that x / ∈ inscopeP (b.i). Therefore by Weakening (lemma 1): L2 ⊢P F1 ∧ · · · ∧ Fn at b.i By the true intro rule: Γ ⊢P true : F1 ∧ · · · ∧ Fn in L2 at b.i. By subsumption: Γ ⊢P true : Γ(x) in L2 at b.i. Conditional Branch Rule In this case, P(b.i) = [x1 : τ1, x2 : τ2] if x3 rop x4 goto b′ and L2 = L1{xj := true}: where L1(x3) = i3, L1(x4) = i4, where j = 1 if ¬(i3 rop i4) and where j = 2 if i3 rop i4. It suffices to show: edgeP (b, b′ ) is an in-edge number for b′ edgeP (b, b + 1) is an in-edge number for (b + 1) b′ .0 and (b + 1).0 are in pcs(P) Γ ⊢P L2 : Γ@dfndAtP (pc, e2) By the context-sensitive syntactic restrictions on programs, b′ must be a block number in the program, and b must not be the last block in the program. Therefore, by definition, b′ .0 and (b + 1).0 are in pcs(P). Also by definition, there are edges in the program (b, b′ ) and (b, b + 1): so the in-edge numbers are likewise well-defined. It remains to show that Γ ⊢P L2 : Γ@dfndAtP (pc, e2). There are two cases: ¬(i3 rop i4) and L1{x1 := true}, or i3 rop i4 and L1{x2 := true}. Suppose ¬(i3 rop i4). By Lemma 2, it suffices to show that Γ ⊢P true : Γ(x1) in L2 at b.i. Since L1(x3) = i3 and L1(x4) = i4, by the environment rule: L1 ⊢P x3 is i3 at b.i L1 ⊢P x4 is i4 at b.i By assumption, ¬(i3 rop i4), so by the comparison rule: L1 ⊢P pf(¬(x3 rop x4)) at b.i So by lemma 1: L2 ⊢P pf(¬(x3 rop x4)) at b.i So by the true introduction rule: Γ ⊢P true : pf(¬(x3 rop x4)) in L2 at b.i By inversion of the typing derivation for the instruction: Γ ⊢ pf(¬(x3 rop x4)) ≤ Γ(x1) Γ ⊢ pf(x3 rop x4) ≤ Γ(x2) So by subsumption: Γ ⊢P true : Γ(x1) in L2 at b.i The argument is similar when i3 rop i4). Goto rule: In this case P(b.i) = goto b′ , L2 = L1, pc = b′ .0, and e2 = edgeP (b, b′ ). By the syntactic restrictions b′ must be a valid block number, so b′ .0 ∈ pcs(P). Since (b, b′ ) is an edge, edgeP (b, b′ ) is a valid in-edge number for b′ . By the definitions dfndAtP (pc, e2) = inscopeP ((b, b′ ).1) = inscopeP (b.i) = dfndAtP (b.i, e1). Thus Γ ⊢P L2 : Γ@dfndAtP (pc, e2) follows from (2). A.4 Progress LEMMA 10 (Env Typing). If Γ ⊢P L : Γ@dfndAtP (pc, n) and x ∈ dfndAtP (pc, n), then L(x) is well defined. Proof: By inversion of the environment typing rules. LEMMA 11 (Progress). If ⊢ S then S is not stuck. POPL ’06 Submission 20 2005/11/15
  • 21. Proof: Assume that ⊢ S and S = (P, L, e, b.i). Recall that by the definition of ⊢ S ⊢ P vt(P) = Γ Γ ⊢P L : Γ@dfndAtP (pc, n) n is an in-edge number for b where pc = b.i pc ∈ pcs(P) And by the definition of ⊢ P P satisfies the SSA property For each x ∈ vt(P), and each y ∈ fv(vt(P)), sdomP (defP (y), defP (x)) vt(P) ⊢ p for every p in P vt(P) ⊢P ι for every instruction ι in P vt(P) ⊢ c for every transfer c in P The proof proceeds by case analysis on P(b.i). p: Let x1 := x2 = p[e] and (b′ , b) be the e’th incoming edge to b (this is well defined by the type rules). By the use/def definition, the instruction is a use of x2 at (b′ , b).1, so by the in-scope property x2 ∈ dfndAtP (b.i, e) (since i = 0). By the definition of ⊢ S above and by lemma 10, note that x2 ∈ dom(L) and hence L(x2) are well defined. Therefore S 7→ (P, L{x1 := L(x2)}, e, b.(i + 1)). x : τ := i: In this case, S 7→ (P, L{x := i}, e, b.(i + 1)). x1 : τ := x2: In this case, since this instruction is a use of x2, by the in-scope property, x2 ∈ inscopeP (b.i). So by definition, x2 ∈ dfndAtP (b.i, e), and so by lemma 10 L(x2) is defined. Therefore S 7→ (P, L{x1 := L(x2)}, e, b.(i + 1)). x1 : τ := newarray(x2, x3): It suffices to show that L(x2) = n for some integer n, and (in the case that n >= 0) that L(x3) = v3 for some value v3. By definition, x2, x3 ∈ inscopeP (b.i), and so by definition, x2.x3 ∈ dfndAtP (b.i, e). Therefore, by 10 L(x2) = v2 and L(x3) = v3 for some v2, v3. It suffices to show that v2 = n for some integer n. By assumption, Γ ⊢ Γ(x2) ≤ int, and Γ ⊢P L : Γ@dfndAtP (b.i, e), so by Canonical Forms (lemma 4), L(x2) = v2 = n for some integer n. x1 : τ := len(x2): It suffices to show that L(x2) = hv0, . . . , vn−1i. By assumption, Γ ⊢ Γ(x2) ≤ array(τ2) and Γ ⊢P L : Γ@dfndAtP (b.i, e), so by Canonical Forms (lemma 4) L(x2) = hv0, . . . , vn−1i for some n. x1 : τ := base(x2): It suffices to show that L(x2) = v, v = hv′i for some v, v′ . By assumption, Γ ⊢ Γ(x2) ≤ array(τ2) and Γ ⊢P L : Γ@dfndAtP (b.i, e), so by Canonical Forms (lemma 4) L(x2) = hv0, . . . , vn−1i for some n. x1 : τ := x2 bop x3: It suffices to show that L(x2) = v2, L(x3) = i3, for some integer i3, and where either v2 = i2 or v2 = v@i2 for some integer i2 and value v. Recall that by assumption, Γ ⊢P L : Γ@dfndAtP (b.i, e), and by the inscope property, x2, x3 ∈ dfndAtP (b.i, e). By assumption, Γ ⊢ Γ(x3) ≤ int so by Canonical Forms (lemma 4) L(x3) = i3. There are two cases to consider for x2, corresponding to the two possible last typing rules of the derivation. 1. Suppose the last rule was the integer operation rule. Then by assumption, Γ ⊢ Γ(x2) ≤ int, and so by Canonical Forms (lemma 4) L(x2) = i2. 2. Suppose the last rule was the managed pointer operation rule. Then by assumption, Γ ⊢ Γ(x2) ≤ ptr?hτ2i, and so by canonical forms, L(x2) = v@i2. x1 : τ := ld(x2) [x3]: It suffices to show that L(x2) = hv0, . . . , vni@i and that 0 ≤ i ≤ n. By assumption, Γ ⊢ Γ(x2) ≤ ptr?hτ2i and by the in-scope property, x2 ∈ dfndAtP (b.i, e), so by Canonical Forms (lemma 4), L(x2) = hv0, . . . , vni@i. Also by assumption, Γ ⊢ Γ(x3) ≤ pf(x@0≤x2∧x2<x2@len(x)), so again by the in-scope property Canonical Forms applies. Therefore, L ⊢P (x@0≤x2 ∧ x2<x2@len(x)) at defP (x3), for some x. Let D be the derivation of L ⊢P (x@0≤x2 ∧ x2<x2@len(x)) at defP (x3). Note that this derivation has a unique last rule. By inversion of D : L ⊢P x@0≤x2 at defP (x3) The derivation of L ⊢P x@0≤x2 at defP (x3) must end in one of the two comparison rules (integer or pointer). Note though that by Canonical Forms (above) L(x2) = hv0, . . . , vni@i, and therefore the only derivation possible for the second premise of the comparison rules is that L ⊢P x2 is hv0, . . . , vni@i2 at defP (x3). POPL ’06 Submission 21 2005/11/15
  • 22. Therefore, by inversion, we have: L ⊢P x@0 is hv0, . . . , vni@i1 at defP (x3) L ⊢P x2 is hv0, . . . , vni@i2 at defP (x3) i1 ≤ i2 By inverting the first sub-derivation, we have: L ⊢P x is hv0, . . . , vni at defP (x3) L ⊢P e is 0 at defP (x3) Therefore, i1 = 0. By inverting the second sub-derivation, we have L(x2) = hv0, . . . , vni@i2, and by the Canonical Forms L(x2) = hv0, . . . , vni@i, so by transitivity, we have i = i2. Finally, recall that i1 ≤ i2, so we have 0 ≤ i. It remains to be shown that i ≤ n. By inversion of D : L ⊢P x2<x2@len(x) at defP (x3) By the same argument as above, this derivation must be a use of the pointer comparison rule. Therefore, by inversion: L ⊢P x2 is hv0, . . . , vni@i2 at defP (x3) L ⊢P x@len(x) is hv0, . . . , vni@ix at defP (x3) i2 < ix By the same argument as above, i2 = i. It therefore suffices to show that ix = n + 1. By inversion of L ⊢P x@len(x) is hv0, . . . , vni@ix at defP (x3): L ⊢P x is hv0, . . . , vni at defP (x3) L ⊢P len(x) is ix at defP (x3) But note that, L(x) = hv0, . . . , vni, so L ⊢P len(x) is n + 1 at defP (x3), and hence ix = n + 1. x1 : τ := pffact(x2): The reduction rule for this instruction always applies. x1 : τ := pfand(x2, x3): The reduction rule for this instruction always applies. [x1 : τ1, x2 : τ2] if x3 rop x4 goto b′ : It suffices to show that: L1(x3) = i3 for some integer i3 L1(x4) = i4 for some integer i4 edgeP (b, b + 1) is well-defined edgeP (b, b′ ) is well-defined Note that x3, x4 ∈ inscopeP (b.i), so x3, x4 ∈ dfndAtP (b.i, e). By assumption: Γ ⊢ Γ(x3) ≤ int Γ ⊢ Γ(x4) ≤ int So by Canonical Forms (lemma 4) L1(x3) = i3 for some integer i3 L1(x4) = i4 for some integer i4 Finally, by definition, (b, b′ ) and (b, b + 1) are in edges(P) and hence edgeP (b, b + 1) and edgeP (b, b′ ) are well-defined. goto b′ : It suffices to show that edgeP (b, b′ ) is well-defined, which follows immediately since by definition, (b, b′ ) is in edges(P). A.5 Type Safety Proof: [of Type Safety] The proof is by induction, Lemma 8, Preservation, and Progress. POPL ’06 Submission 22 2005/11/15