400 likes | 476 Views
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.
E N D
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. int sum(int[] items) { int r = 0; for(int v : items){ r += v; } return r; } booleanisSorted(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 Pointer Analysis Purity Inference Annotated Source Java Source ? Annotated Bytecode
Modular Purity System Java Compiler Purity Inference Annotated Source • Purity Inference: • Generates annotations via interproceduralanalysis • Generated annotations are modularly checkable • Purity Checker: • Verifies annotations via intraprocedural analysis • Integrates easily with Java Bytecode Verification Java Source Purity Checker Annotated Bytecode
Simple (Modular) Approach • Pure Methods: • Cannot contain field assignments • Can only call methods marked @Pure • Only pure methods can override pure methods Parent privateint 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 privateint afield; @Pure void method() { } Child @Pure void method() { } Client @Pure void f(Parent p) { p.method(); }
Simple (Modular) Approach Parent privateint f; @Pure void method(){f=1;} • 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 privateint 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 privateint afield; @Pure void method() { } Child @Pure void method() { } Client @Pure void f(Parent p) { p.method(); }
Problems publicclass Test { private List<String> items; @Pure boolean has(String s) { for(String i : items) { if(s == i) { return true; } } return false; } } public classAbstractStringBuilder { 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”; }
Problems publicclass Test { private List<String> items; @Pure boolean has(String s) { for(String i : items) { if(s == i) { return true; } } return false; } } public classAbstractStringBuilder { 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”; }
Problems publicclass Test { private List<String> items; @Pure boolean has(String s) { for(String i : items) { if(s == i) { return true; } } return false; } } public classAbstractStringBuilder { 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”; }
Problems publicclass Test { private List<String> items; @Pure boolean has(String s) { for(String i : items) { if(s == i) { return true; } } return false; } } public classAbstractStringBuilder { 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”; }
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 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; } } Indicates next() only modifies “local” state
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; } } Indicates next() only modifies “local” state
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; } } Indicates next() only modifies “local” state @Pure boolean has(String s) { Iterator tmp; tmp = items.iterator(); while(tmp.hasNext()) { i = tmp.next(); 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; } } Indicates next() only modifies “local” state @Pure boolean has(String s) { Iterator tmp; tmp = items.iterator(); while(tmp.hasNext()) { i = tmp.next(); if(s == i) return true; } return false; }
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++]; } }} • 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
Iterator Implementation class ArrayList implements Collection{ … @Fresh Object iterator() { return new Iterator(data); } static class Iterator { Object[] data; intidx = 0; … @Pure booleanhasNext() { return idx < data.size(); } @Local Object next() { return data[idx++]; } }} • 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 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]; } }} TList data length,type Locality Invariant 1 (Construction). When a new object is constructed its locality is always fresh. Locality Invariant 2 (Preservation). When the locality of a fresh object is modified, its locality must remain 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]; } }} TList data length,type Required for Invariant 1 Locality Invariant 1 (Construction). When a new object is constructed its locality is always fresh. Locality Invariant 2 (Preservation). When the locality of a fresh object is modified, its locality must remain 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]; } }} TList data length,type Required for Invariant 1 Safe under Invariant 2 Locality Invariant 1 (Construction). When a new object is constructed its locality is always fresh. Locality Invariant 2 (Preservation). When the locality of a fresh object is modified, its locality must remain fresh.
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ?
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ?
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ? LTHIS LDST ?
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ? LTHIS LDST ? LTHIS LDST
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ? LTHIS LDST ? LTHIS LDST LTHIS LDST
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ? LTHIS LDST ? LTHIS LDST LTHIS LDST LTHIS LDST
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ? LTHIS LDST ? LTHIS LDST LTHIS LDST LTHIS LDST LDST LTHIS LDST
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ? LTHIS LDST ? LTHIS LDST LTHIS LDST LTHIS LDST LDST LTHIS LDST LTHIS LDST LDST
Detailed Example this dst tmp @Local public void copy(TListdst) { vartmp = 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 ? LTHIS LDST LTHIS LDST LTHIS LDST ? LTHIS LDST ? LTHIS LDST LTHIS LDST LTHIS LDST LDST LDST LTHIS LDST LTHIS 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 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; }} • Disappointment! • Object.equals()not inferred @Pure • Object.hashCode() not inferred @Pure
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 Law of Locality. When checking @Local annotations, one can safely assume parameters are not aliased (!) • 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; }}