jpure: a modular purity system for java
DESCRIPTION
JPure: a Modular Purity System for Java. David J. Pearce Victoria University of Wellington New Zealand. Introduction. Definition: A method is considered pure if it does not assign (directly of indirectly) to any field or array cell that existed before it was called. - PowerPoint PPT PresentationTRANSCRIPT
JPure: a Modular Purity System for Java
David J. PearceVictoria University of Wellington
New Zealand
Introduction
int sum(int[] items) {
int r = 0;
for(int v : items) { r += v; }
return r;
}
Definition: A method is considered pure if it does not assign (directly of indirectly) to any field or array cell that existed before it was called.
boolean isSorted(List<Integer> items) {
int last = Integer.MIN_VALUE;
for(Integer v : items) {
if(last > v) { return false; }
last = v;
}
return true;
}
Typical Previous Purity Systems
• Pointer Analysis feeds Purity Inference
– Pointer Analysis typically whole-program
– Inferred annotations cannot be checked easily• i.e. without regeneration pointer information
PointerAnalysis
?
Java Source Annotated Source
Annotated Bytecode
PurityInference
Modular Purity System
• Purity Inference:
– Generates annotations via interprocedural analysis
– Generated annotations are modularly checkable
• Purity Checker:
– Verifies annotations via intraprocedural analysis
– Integrates easily with Java Bytecode Verification
PurityInference
PurityChecker
Java Source Annotated Source
Annotated Bytecode
Java Compiler
Simple (Modular) Approach
• Pure Methods:
– Cannot contain field assignments
– Can only call methods marked @Pure
– Only pure methods can override pure methods
Parent
private int afield;
@Pure void method() { }
Child
@Pure void method() { }
Client
@Pure void f(Parent p) {
p.method();
}
Simple (Modular) Approach
• Pure Methods:
– Cannot contain field assignments
– Can only call methods marked @Pure
– Only pure methods can override pure methods
Parent
private int afield;
@Pure void method() { }
Child
@Pure void method() { }
Client
@Pure void f(Parent p) {
p.method();
}
Simple (Modular) Approach
• Pure Methods:
– Cannot contain field assignments
– Can only call methods marked @Pure
– Only pure methods can override pure methods
Parent
private int f;
@Pure void method(){f=1;}
Child
@Pure void method() { }
Client
@Pure void f(Parent p) {
p.method();
}
Simple (Modular) Approach
• Pure Methods:
– Cannot contain field assignments
– Can only call methods marked @Pure
– Only pure methods can override pure methods
Child
@Pure void method() { }
Client
@Pure void f(Parent p) {
p.method();
}
Simple (Modular) Approach
• Pure Methods:
– Cannot contain field assignments
– Can only call methods marked @Pure
– Only pure methods can override pure methods
Parent
private int afield;
@Pure void method() { }
Client
@Pure void f(Parent p) {
p.method();
}
Problems
public class AbstractStringBuilder {
private char[] data;
private int count; // number of items used
public AbstractStringBuilder append(String s) {
…
s.getChars(0, s.length(), data, count);
}}
@Pure String f(String x) { return x + “hello”; }
public class Test {
private List<String> items;
@Pure boolean has(String s) {
for(String i : items) {
if(s == i) { return true; }
}
return false;
} }
Problems
public class AbstractStringBuilder {
private char[] data;
private int count; // number of items used
public AbstractStringBuilder append(String s) {
…
s.getChars(0, s.length(), data, count);
}}
@Pure String f(String x) { return x + “hello”; }
public class Test {
private List<String> items;
@Pure boolean has(String s) {
for(String i : items) {
if(s == i) { return true; }
}
return false;
} }
Problems
public class AbstractStringBuilder {
private char[] data;
private int count; // number of items used
public AbstractStringBuilder append(String s) {
…
s.getChars(0, s.length(), data, count);
}}
@Pure String f(String x) { return x + “hello”; }
public class Test {
private List<String> items;
@Pure boolean has(String s) {
for(String i : items) {
if(s == i) { return true; }
}
return false;
} }
Problems
public class AbstractStringBuilder {
private char[] data;
private int count; // number of items used
public AbstractStringBuilder append(String s) {
…
s.getChars(0, s.length(), data, count);
}}
@Pure String f(String x) { return x + “hello”; }
public class Test {
private List<String> items;
@Pure boolean has(String s) {
for(String i : items) {
if(s == i) { return true; }
}
return false;
} }
interface Collection {
@Fresh Object iterator();
}
interface Iterator {
@Pure boolean hasNext();
@Local Object next();
}
class Test {
List<String> items;
@Pure boolean has(String s){
for(String i : items) {
if(s == i) return true;
}
return false;
} }
Introducing JPure!
interface Collection {
@Fresh Object iterator();
}
interface Iterator {
@Pure boolean hasNext();
@Local Object next();
}
class Test {
List<String> items;
@Pure boolean has(String s){
for(String i : items) {
if(s == i) return true;
}
return false;
} }
Introducing JPure!Indicates iterator() returns “fresh” object
interface Collection {
@Fresh Object iterator();
}
interface Iterator {
@Pure boolean hasNext();
@Local Object next();
}
class Test {
List<String> items;
@Pure boolean has(String s){
for(String i : items) {
if(s == i) return true;
}
return false;
} }
Introducing JPure!Indicates iterator() returns “fresh” object
Indicates next() only modifies “local” state
interface Collection {
@Fresh Object iterator();
}
interface Iterator {
@Pure boolean hasNext();
@Local Object next();
}
class Test {
List<String> items;
@Pure boolean has(String s){
for(String i : items) {
if(s == i) return true;
}
return false;
} }
Introducing JPure!Indicates iterator() returns “fresh” object
Indicates next() only modifies “local” state
interface Collection {
@Fresh Object iterator();
}
interface Iterator {
@Pure boolean hasNext();
@Local Object next();
}
class Test {
List<String> items;
@Pure boolean has(String s){
for(String i : items) {
if(s == i) return true;
}
return false;
} }
Introducing JPure!
@Pure boolean has(String s) {
Iterator tmp;
tmp = items.iterator();
while(tmp.hasNext()) {
i = tmp.next();
if(s == i) return true;
}
return false;
}
Indicates iterator() returns “fresh” object
Indicates next() only modifies “local” state
interface Collection {
@Fresh Object iterator();
}
interface Iterator {
@Pure boolean hasNext();
@Local Object next();
}
class Test {
List<String> items;
@Pure boolean has(String s){
for(String i : items) {
if(s == i) return true;
}
return false;
} }
Introducing JPure!
@Pure boolean has(String s) {
Iterator tmp;
tmp = items.iterator();
while(tmp.hasNext()) {
i = tmp.next();
if(s == i) return true;
}
return false;
}
Indicates iterator() returns “fresh” object
Indicates next() only modifies “local” state
• Methods annotated @Fresh– Must return new objects– Or, values returned by methods marked @Fresh
• Methods annotated @Local– May update “local” state … – But otherwise must remain pure
class ArrayList implements Collection{
…
@Fresh Object iterator() { return new Iterator(data); }
static class Iterator {
Object[] data; int idx = 0;
…
@Pure boolean hasNext() { return idx < data.size(); }
@Local Object next() { return data[idx++]; }
}}
Iterator Implementation
• Methods annotated @Fresh– Must return new objects– Or, values returned by methods marked @Fresh
• Methods annotated @Local– May update “local” state … – But otherwise must remain pure
class ArrayList implements Collection{ …
@Fresh Object iterator() { return new Iterator(data); }
static class Iterator {
Object[] data; int idx = 0; …
@Pure boolean hasNext() { return idx < data.size(); }
@Local Object next() { return data[idx++]; }
}}
Locality Invariant 2 (Preservation). When the locality of a fresh object is modified, its locality must remain fresh.
Locality Invariant 1 (Construction). When a new object is constructed its locality is always fresh.
class TList {
private int length;
private @Local Object[] data;
private Type type;
@Local public TList(Type t, int m) {
length = 0;
data = new Object[m];
type = t;
}
@Local public void copy(TList dst) {
length = dst.length;
type = dst.type;
data = new Object[dst.length];
for(int i=0;i!=length;++i) { data[i] = dst.data[i]; }
}}
TListdata
length,type
Locality Invariant 2 (Preservation). When the locality of a fresh object is modified, its locality must remain fresh.
Locality Invariant 1 (Construction). When a new object is constructed its locality is always fresh.
class TList {
private int length;
private @Local Object[] data;
private Type type;
@Local public TList(Type t, int m) {
length = 0;
data = new Object[m];
type = t;
}
@Local public void copy(TList dst) {
length = dst.length;
type = dst.type;
data = new Object[dst.length];
for(int i=0;i!=length;++i) { data[i] = dst.data[i]; }
}}
TListdata
length,type
Required forInvariant 1
Locality Invariant 2 (Preservation). When the locality of a fresh object is modified, its locality must remain fresh.
Locality Invariant 1 (Construction). When a new object is constructed its locality is always fresh.
class TList {
private int length;
private @Local Object[] data;
private Type type;
@Local public TList(Type t, int m) {
length = 0;
data = new Object[m];
type = t;
}
@Local public void copy(TList dst) {
length = dst.length;
type = dst.type;
data = new Object[dst.length];
for(int i=0;i!=length;++i) { data[i] = dst.data[i]; }
}}
TListdata
length,type
Required forInvariant 1
Safe underInvariant 2
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
LTHIS LDST ?
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
LTHIS LDST ?
LTHIS LDST
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
LTHIS LDST ?
LTHIS LDST
LTHIS LDST
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
LTHIS LDST ?
LTHIS LDST
LTHIS LDST
LTHIS LDST
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
LTHIS LDST ?
LTHIS LDST
LTHIS LDST
LTHIS LDST
LTHIS LDST LDST
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
LTHIS LDST ?
LTHIS LDST
LTHIS LDST
LTHIS LDST
LTHIS LDST
LTHIS LDST
LDST
LDST
Detailed Example@Local public void copy(TList dst) {
var tmp = dst.length;
this.length = tmp;
tmp = dst.type;
this.type = tmp;
tmp = new Object[dst.length];
this.data = tmp;
for(int i=0;i!=length;++i) {
tmp = dst.data[i];
this.data[i] = tmp;
} }
LTHIS LDST ?
tmpdstthis
LTHIS LDST
LTHIS LDST
LTHIS LDST ?
LTHIS LDST ?
LTHIS LDST
LTHIS LDST
LTHIS LDST
LTHIS LDST
LTHIS LDST
LDST
LDST
LDST
Checking vs Inference
• Purity Checker:
– Intraprocedural dataflow analysis
– Uses static information about called methods
– Checks fresh objects flow to @Fresh returns
– Checks assignments to @Local fields are fresh
– Checks assignments to other fields are in locality
– Checks annotations overridden correctly
• Purity Inference:
– Interprocedural dataflow analysis
– Uses static call graph
– Essentially works in opposite direction to checker
– E.g. if all returned values fresh -> method annotated @Fresh
Limitations
• Disappointment!– Object.equals() not inferred @Pure– Object.hashCode() not inferred @Pure
class Test {
private int hashCode;
public boolean equals(Object o) {
if(hashCode() == o.hashCode()) { … }
return false;
}
public int hashCode() {
if(hashCode == -1) { hashCode = …; }
return hashCode;
}}
Conclusion
• The JPure System
– Built around Modularly Checkable Annotations
– Interprocedural analysis infers annotations
– Intraprocedural analysis checks annotations• Could be incorporated in Java Bytecode Verifier
– Locality & freshness help uncover more purity• 41% on average for benchmarks (vs 25% for simple)
See http://www.ecs.vuw.ac.nz/~djp/jpure
Law of Locality
• Example:
– What if other aliased with this?
– Applying Law of Locality seems counter-intuitive
class Test {
private int field;
@Local void f(Test other){
this.field = 1;
}}
Law of Locality. When checking @Local annotations, one can safely assume parameters are not aliased (!)