// Copyright (c) 2011, David J. Pearce (djp@ecs.vuw.ac.nz) // All rights reserved. // // This software may be modified and distributed under the terms // of the BSD license. See the LICENSE file for details. package wyil.lang; import java.util.*; import wybs.lang.NameID; import wycc.util.ArrayUtils; import wycc.util.Pair; import wyil.util.TypeSystem; import wyil.util.type.TypeParser; /** * A structural type. See * http://whiley.org/2011/03/07/implementing-structural-types/ for more on how * this class works. * * @author David J. Pearce * */ public interface Type { // ============================================================= // Interface // ============================================================= /** * Represents a type which constitutes a "leaf" node in the type tree. That * is, it contains no types as subcomponents. * * @author David J. Pearce * */ public interface Leaf extends Type { } /** * Represents a primitive type (e.g. int, bool, null, etc). * * @author David J. Pearce * */ public interface Primitive extends Leaf { } /** * Represents a named type within the system. That is, this type refers by * name to a given type declaration somewhere in the overall program. * * @author David J. Pearce * */ public interface Nominal extends Leaf { public NameID name(); } /** * An effective array is a type which looks like an array, but is not * exactly an array. For example, a union of arrays is an effective array. * This is because in some situations we can read elements from the array, * and also write elements to the array. * * @author David J. Peace * */ public interface EffectiveArray extends Type { /** * Get the element type which could be read from this array. * * @return */ public Type getReadableElementType(); /** * Get the element type which could be written to this array. * * @return */ public Type getWriteableElementType(); /** * Determine a new type for this array after an assignment to a given * element. * * @param element * The type of the element being assigned */ public EffectiveArray update(Type element); } /** * An array type describes array values whose elements are subtypes of the * element type. For example, <code>[1,2,3]</code> is an instance of array * type <code>int[]</code>; however, <code>[false]</code> is not. * * @author David J. Pearce * */ public interface Array extends EffectiveArray { /** * Get the element type of this array. All values in any instances of * this type must be instances of the element types. */ public Type element(); } /** * An effective record is a type which looks like a record, but is not * exactly a record. For example, a union of records is an effective record. * This is because in some situations we can read fields from the record, * and also write fields to the record. * * @author David J. Peace * */ public interface EffectiveRecord extends Type { /** * Return the number of fields in this type * * @return */ public int size(); /** * Check whether a given field is present in this effective record or * not. * * @param field * @return */ public boolean hasField(String field); /** * Determine the index of a given field in this effective record. * * @param field * @return */ public int getFieldIndex(String field); /** * Get the array of fields used in this type. */ public String[] getFieldNames(); /** * Get the element type which could be read from this array. * * @return */ public Type getReadableFieldType(String field); /** * Get the element type which could be written to this array. * * @return */ public Type getWriteableFieldType(String field); /** * Get an updated version of this record type after a given field has * been assigned a given type. * * @param field * The field name being assigned. This must be an explicit * field declared in this type. * @param type * The type of the value being assigned to the given field. * @return */ public Type.EffectiveRecord update(String field, Type type); } /** * A record is made up of a number of fields, each of which has a unique * name. Each field has a corresponding type. One can think of a record as a * special kind of "fixed" map (i.e. where we know exactly which entries we * have). * * @author David J. Pearce * */ public interface Record extends EffectiveRecord { /** * Get the number of fields in this record */ @Override public int size(); /** * Check whether this is an open record or not. That is, whether or not * there are additional "unknown" fields in this record. * * @return */ public boolean isOpen(); /** * Get the type of a given field in this record */ public Type getField(String name); } /** * An effective reference is a type which looks like an reference, but is * not exactly an reference. For example, a union of references is an * effective reference. This is because in some situations we can read from * the reference, and also write through the reference. * * @author David J. Peace * */ public interface EffectiveReference extends Type { /** * Get the element type which could be read from this array. * * @return */ public Type getReadableElementType(); /** * Get the element type which could be written to this array. * * @return */ public Type getWriteableElementType(); } /** * Represents a reference to an object in Whiley. For example, * <code>&this:int</code> is the type of a reference to a location allocated * in the enclosing scope which holds an integer value. * * @author David J. Pearce * */ public interface Reference extends EffectiveReference { /** * Return the type of the location that this reference refers to. It is * an invariant that the type does not change for the life of the * location. * * @return */ public Type element(); /** * Return the lifetime of this reference. That is a symbolic name which * declared in the enclosing scope, or "*" in the case of the global * lifetime. * * @return */ public String lifetime(); } /** * Represents the set of types which are not in a given type. For example, * <code>!int</code> is the set of all values which are not integers. Thus, * for example, the type <code>bool</code> is a subtype of <code>!int</code> * . * * @author David J. Pearce * */ public interface Negation extends Type { /** * Get the element type of this array. All values in any instances of * this type must be instances of the element types. */ public Type element(); } /** * Represents the set of all functions or methods. These are values which * can be called using an indirect invoke expression. Each function or * method accepts zero or more parameters and will produce zero or more * returns. * * @author David J. Pearce * */ public interface FunctionOrMethod extends Type { /** * Get the list of parameter types which are accepted by this function * or method. */ Type[] params(); /** * Get the list of types which are returned by this function or method. * * @return */ Type[] returns(); /** * Get the ith parameter type * * @param i * @return */ Type parameter(int i); } /** * Represents the set of all function values. These are pure functions, * sometimes also called "mathematical" functions. A function cannot have * any side-effects and must always return the same values given the same * inputs. A function cannot have zero returns, since this would make it a * no-operation. * * @author David J. Pearce * */ public interface Function extends FunctionOrMethod { } /** * Represents the set of all method values. These are impure and may have * side-effects (e.g. performing I/O, updating non-local state, etc). A * method may have zero returns and, in such case, the effect of a method * comes through other side-effects. Methods may also have contextual * lifetime arguments, and may themselves declare lifetime arguments. * * @author David J. Pearce * */ public interface Method extends FunctionOrMethod { /** * Get the context lifetimes for this method. A contextual lifetime * identifies a lifetime to which this method is bound, but which is not * explicit in the parameters or returns of the method. This can arise * through the construction of lambda methods, but also through the use * oc currying. * * @return */ public String[] contextLifetimes(); /** * Get the lifetime parameters declared by this method. These are * essentially additional parameters to the method, and reference * parameters or returns may only be expressed in terms of these * lifetimes (and the global lifetime). * * @return */ public String[] lifetimeParams(); } /** * Represents the set of all proeprty values. These are pure predicates, * sometimes also called "mathematical" functions. A property cannot have * any side-effects and always returns the boolean true. * * @author David J. Pearce * */ public interface Property extends FunctionOrMethod { } /** * Represents the intersection of one or more types together. For example, * the intersection of <code>T1</code> and <code>T2</code> is * <code>T1&T2</code>. Furthermore, any variable of this type must be both * an instanceof <code>T1</code> and an instanceof <code>T2</code>. * * @author David J. Pearce * */ public interface Intersection extends Type { public Type[] bounds(); } /** * Represents the union of one or more types together. For example, the * union of <code>int</code> and <code>null</code> is <code>int|null</code>. * Any variable of this type may hold any integer or the null value. * Furthermore, the types <code>int</code> and <code>null</code> are * collectively referred to as the "bounds" of this type. * * @author David J. Pearce * */ public interface Union extends Type { public Type[] bounds(); } // ============================================================= // Constructors // ============================================================= /** * The type any represents the type whose variables may hold any possible * value. <b>NOTE:</b> the any type is top in the type lattice. * * @author David J. Pearce * */ public static final Type T_ANY = new Impl.Primitive(TypeSystem.K_ANY) { @Override public String toString() { return "any"; } }; /** * A void type represents the type whose variables cannot exist! That is, * they cannot hold any possible value. Void is used to represent the return * type of a function which does not return anything. However, it is also * used to represent the element type of an empty list of set. <b>NOTE:</b> * the void type is a subtype of everything; that is, it is bottom in the * type lattice. * * @author David J. Pearce * */ public static final Type T_VOID = new Impl.Primitive(TypeSystem.K_VOID) { @Override public String toString() { return "void"; } }; /** * The null type is a special type which should be used to show the absence * of something. It is distinct from void, since variables can hold the * special <code>null</code> value (where as there is no special "void" * value). With all of the problems surrounding <code>null</code> and * <code>NullPointerException</code>s in languages like Java and C, it may * seem that this type should be avoided. However, it remains a very useful * abstraction to have around and, in Whiley, it is treated in a completely * safe manner (unlike e.g. Java). * * @author David J. Pearce * */ public static final Type T_NULL = new Impl.Primitive(TypeSystem.K_NULL) { @Override public String toString() { return "null"; } }; /** * Represents the set of boolean values (i.e. true and false) * * @author David J. Pearce * */ public static final Type T_BOOL = new Impl.Primitive(TypeSystem.K_BOOL) { @Override public String toString() { return "bool"; } }; /** * Represents a sequence of 8 bits. Note that, unlike many languages, there * is no representation associated with a byte. For example, to extract an * integer value from a byte, it must be explicitly decoded according to * some representation (e.g. two's compliment) using an auxillary function * (e.g. <code>Byte.toInt()</code>). * * @author David J. Pearce * */ public static final Type T_BYTE = new Impl.Primitive(TypeSystem.K_BYTE) { @Override public String toString() { return "byte"; } }; /** * Represents the set of (unbound) integer values. Since integer types in * Whiley are unbounded, there is no equivalent to Java's * <code>MIN_VALUE</code> and <code>MAX_VALUE</code> for <code>int</code> * types. * * @author David J. Pearce * */ public static final Type T_INT = new Impl.Primitive(TypeSystem.K_INT) { @Override public String toString() { return "int"; } }; /** * The type meta represents the type of types. That is, values of this type * are themselves types. (think reflection, where we have * <code>class Class {}</code>). * * @author David J. Pearce * */ public static final Type T_META = new Impl.Primitive(TypeSystem.K_META) { @Override public String toString() { return "meta"; } }; public static Type Nominal(NameID name) { return new Impl.Nominal(name); } public static Type Record(boolean isOpen, List<Pair<Type, String>> fields) { Pair<Type, String>[] arr = new Pair[fields.size()]; fields.toArray(arr); return Record(isOpen, arr); } public static Type Record(boolean isOpen, Pair<Type, String>... fields) { // Sort arrays by their field Arrays.sort(fields, Impl.FIELD_COMPARATOR); // Sanity check no two fields with same name, and no void element types for (int i = 0; i != fields.length; ++i) { Pair<Type, String> field = fields[i]; Type type = field.first(); String name = field.second(); if (type == T_VOID) { // A void element type is not a logical error, but it does mean // this type reduces immediately to void return type; } else if (i > 0 && fields[i - 1].second().equals(name)) { // This indicates we have two fields with the same name. This is // a logical error. throw new IllegalArgumentException("duplicate field encountered:" + name); } } // Create the record return new Impl.Record(isOpen, (Pair[]) fields); } public static Type Array(Type element) { if (element == T_VOID) { return T_VOID; } else { return new Impl.Array((Impl) element); } } public static Type Reference(String lifetime, Type element) { return new Impl.Reference((Impl) element, lifetime); } public static Type Function(Type[] parameters, Type[] returns) { Impl[] iParameters = toImplOrVoid(parameters); Impl[] iReturns = toImplOrVoid(returns); if (iParameters == null || iReturns == null) { return T_VOID; } else { return new Impl.Function(iParameters, iReturns); } } public static Type Property(Type[] parameters) { Impl[] iParameters = toImplOrVoid(parameters); if (iParameters == null) { return T_VOID; } else { return new Impl.Property(iParameters); } } public static Type Method(Type[] parameters, Type[] returns) { return Method(new String[0], new String[0], parameters, returns); } public static Type Method(Collection<String> lifetimeParameters, Collection<String> contextLifetimes, Type[] parameters, Type[] returns) { String[] lifetimes = ArrayUtils.toStringArray(lifetimeParameters); String[] contexts = ArrayUtils.toStringArray(contextLifetimes); // Need to sort contexts and remove duplicates. Arrays.sort(contexts); contexts = ArrayUtils.sortedRemoveDuplicates(contexts); // return Method(lifetimes, contexts, parameters, returns); } public static Type Method(String[] lifetimeParameters, String[] contextLifetimes, Type[] parameters, Type[] returns) { Impl[] iParameters = toImplOrVoid(parameters); Impl[] iReturns = toImplOrVoid(returns); if (iParameters == null || iReturns == null) { return T_VOID; } else { // FIXME: should we be sorting the context lifetimes? return new Impl.Method(lifetimeParameters, contextLifetimes, iParameters, iReturns); } } static Impl[] toImplOrVoid(Type[] types) { Impl[] impls = new Impl[types.length]; for (int i = 0; i != types.length; ++i) { Type type = types[i]; if (type == T_VOID) { return null; } else { impls[i] = (Impl) type; } } return impls; } static Impl.Array[] toImplArrays(Impl[] types) { Impl.Array[] impls = new Impl.Array[types.length]; for (int i = 0; i != types.length; ++i) { impls[i] = (Impl.Array) types[i]; } return impls; } static Impl.Record[] toImplRecords(Impl[] types) { Impl.Record[] impls = new Impl.Record[types.length]; for (int i = 0; i != types.length; ++i) { impls[i] = (Impl.Record) types[i]; } return impls; } /** * Construct the negation of a given type. This operation performs certain * simplifications to ensure types are in a manageable form. For example, * the negation of a negation is just the element type. Likewise, the * negation of a union is an intersection of negations, etc. * * @param types * @return */ public static Type Negation(Type type) { if (type instanceof Type.Union) { // Apply De-Mogan's law. So, !(A | B) => !A & !B Type.Union union = (Type.Union) type; Type[] bounds = union.bounds(); Type[] nBounds = new Type[bounds.length]; for (int i = 0; i != bounds.length; ++i) { nBounds[i] = Negation(bounds[i]); } return Intersection(nBounds); } else if (type instanceof Type.Intersection) { // Apply De-Mogan's law. So, !(A & B) => !A | !B Type.Intersection intersection = (Type.Intersection) type; Type[] bounds = intersection.bounds(); Type[] nBounds = new Type[bounds.length]; for (int i = 0; i != bounds.length; ++i) { nBounds[i] = Negation(bounds[i]); } return Intersection(nBounds); } else if (type instanceof Type.Negation) { // Here, !!T => T Type.Negation negation = (Type.Negation) type; return negation.element(); } else { return new Impl.Negation((Impl.Atom) type); } } /** * Construct the union of one or more types together. This operation * performs certain simplifications to ensure types are in a manageable * form. For example, the union of one type is that type. Likewise, the * union of two identical types is just one type, etc. * * <b>NOTE</b> this operation assumes free access to modify the given array * * @param types * @return */ public static Type Union(Type... types) { // At this point, we can assume that the given types are themselves // simplified. This helps as, for example, it means we cannot // encounter a unions of in the parameters. // Impl.Conjunctable[] cs = Impl.toConjunctables(types); // Apply obvious simplifications if (ArrayUtils.firstIndexOf(cs, T_ANY) >= 0) { // Any union containing any equals any return T_ANY; } cs = ArrayUtils.<Impl.Conjunctable> removeAll(cs, (Impl.Conjunctable) T_VOID); cs = ArrayUtils.removeDuplicates(cs); // if (cs.length == 0) { return Type.T_VOID; } else if (cs.length == 1) { return cs[0]; } else { // Ensure elements in sorted order Arrays.sort(cs); // Determine whether have union of a specific type or not. int kind = Impl.determineCommonKind(cs); switch (kind) { case TypeSystem.K_RECORD: return new Impl.UnionOfRecords(toImplRecords(cs)); case TypeSystem.K_ARRAY: return new Impl.UnionOfArrays(toImplArrays(cs)); case TypeSystem.K_REFERENCE: // FIXME: return UnionOfReferencess default: return new Impl.UnionOfConjunctables(cs); } } } /** * Construct the intersection of one or more types together. This operation * performs certain simplifications to ensure types are in a manageable * form. For example, the intersection of one type is that type. Likewise, * the intersection of two identical types is just one type, etc. * * <b>NOTE</b> this operation assumes free access to modify the given array * * @param types * @return */ public static Type Intersection(Type... types) { // At this point, we can assume that the given types are themselves // simplified. This helps as, for example, it means we cannot // encounter a unions of in the parameters. // // Extract and distribute over any nested unions int unionIndex = Impl.findUnion(types); if (unionIndex >= 0) { // Yes, there is at least one union we can distribute over return Impl.distributeUnion(unionIndex, types); } else { // Flatten any nested intersections so they are all at the same // level. Impl.Atom[] atoms = Impl.toAtoms(types); atoms = ArrayUtils.removeDuplicates(atoms); // Intersect all remaining atoms. This may be // result in void being discovered. for (int i = 0; i < atoms.length; ++i) { for (int j = i + 1; j < atoms.length; ++j) { Impl.intersectAtoms(i, j, atoms); } } // Check whether found void or not. if (ArrayUtils.firstIndexOf(atoms, T_VOID) >= 0) { // Any intersection containing void equals void return T_VOID; } // Finally, tidy up the remainder by eliminating any null values // that have been created. Null values can be created when // intersection certain types together. For example, intersection // int with !bool generate int and null. atoms = ArrayUtils.<Impl.Atom> removeAll(atoms, (Impl.Atom) T_ANY); // if (atoms.length == 0) { return Type.T_ANY; } else if (atoms.length == 1) { return atoms[0]; } else { // Ensure elements in sorted order Arrays.sort(atoms); // Construct the conjunct return new Impl.Conjunct(atoms); } } } public static Type fromString(String str) { return new TypeParser(str).parse(); } // ============================================================= // Implementation // ============================================================= abstract static class Impl implements Type, Comparable<Impl> { public abstract int getKind(); /** * Represents either an atom or a conjunct * * @author David J. Pearce * */ public abstract static class Conjunctable extends Impl { } /** * An atom represents an indivisible type. More specifically, a type * which does not contain a union or intersection anywhere, including in * subcomponents. * * @author David J. Pearce * */ public abstract static class Atom extends Conjunctable { } /** * A positive atom is any atom except a negation * * @author David J. Pearce * */ public abstract static class PositiveAtom extends Atom { } public abstract static class Primitive extends PositiveAtom implements Type.Primitive { private final int kind; private Primitive(int kind) { this.kind = kind; } @Override public abstract String toString(); @Override public int getKind() { return kind; } @Override public boolean equals(Object o) { if (o instanceof Impl.Primitive) { Impl.Primitive p = (Impl.Primitive) o; return kind == p.kind; } return false; } @Override public int hashCode() { return kind; } @Override public int compareTo(Impl p) { return kind - p.getKind(); } } private static final class Nominal extends Atom implements Type.Nominal { private NameID nid; public Nominal(NameID name) { nid = name; } @Override public boolean equals(Object o) { if (o instanceof Impl.Nominal) { Impl.Nominal e = (Impl.Nominal) o; return nid.equals(e.nid); } return false; } @Override public NameID name() { return nid; } @Override public int hashCode() { return nid.hashCode(); } @Override public String toString() { return nid.toString(); } @Override public int getKind() { return TypeSystem.K_NOMINAL; } @Override public int compareTo(Impl p) { int r = TypeSystem.K_NOMINAL - p.getKind(); if (r == 0) { Impl.Nominal n = (Impl.Nominal) p; return nid.compareTo(n.nid); } else { return r; } } } private static final class Reference extends PositiveAtom implements Type.Reference { private final Impl element; private final String lifetime; public Reference(Impl element, String lifetime) { this.element = element; this.lifetime = lifetime; } @Override public Type element() { return element; } @Override public Type getReadableElementType() { return element; } @Override public Type getWriteableElementType() { return element; } @Override public String lifetime() { return lifetime; } @Override public int getKind() { return TypeSystem.K_REFERENCE; } @Override public boolean equals(Object o) { if (o instanceof Impl.Reference) { Impl.Reference r = (Impl.Reference) o; return element.equals(r.element) && lifetime.equals(r.lifetime); } return false; } @Override public int compareTo(Impl p) { int r = TypeSystem.K_REFERENCE - p.getKind(); if (r == 0) { Impl.Reference n = (Impl.Reference) p; r = element.compareTo(n.element); if (r == 0) { return lifetime.compareTo(n.lifetime); } } return r; } @Override public int hashCode() { return element.hashCode() + lifetime.hashCode(); } @Override public String toString() { String body = element.toString(); if (element instanceof Union || element instanceof Intersection || element instanceof Negation || element instanceof Array) { body = "(" + body + ")"; } if(lifetime.equals("*")) { return "&" + body; } else { return "&" + lifetime + ":" + body; } } } private static final class Array extends PositiveAtom implements Type.Array { private final Impl element; public Array(Impl element) { this.element = element; } @Override public Type element() { return element; } @Override public Type getReadableElementType() { return element; } @Override public Type getWriteableElementType() { return element; } @Override public int getKind() { return TypeSystem.K_ARRAY; } @Override public Impl.Array update(Type newElement) { return new Impl.Array((Impl) Type.Union(element, newElement)); } @Override public boolean equals(Object o) { if (o instanceof Impl.Array) { Impl.Array a = (Impl.Array) o; return element.equals(a.element); } return false; } @Override public int compareTo(Impl p) { int r = TypeSystem.K_ARRAY - p.getKind(); if (r == 0) { Impl.Array n = (Impl.Array) p; return element.compareTo(n.element); } return r; } @Override public int hashCode() { return element.hashCode() * 2; } @Override public String toString() { String body = element.toString(); if (element instanceof Union || element instanceof Intersection || element instanceof Negation || element instanceof Reference) { return "(" + element.toString() + ")[]"; } else { return body + "[]"; } } } private static final Comparator<Pair<Type, String>> FIELD_COMPARATOR = new Comparator<Pair<Type, String>>() { @Override public int compare(Pair<Type, String> o1, Pair<Type, String> o2) { return o1.second().compareTo(o2.second()); } }; private static final class Record extends PositiveAtom implements Type.Record { private final Pair<Impl, String>[] fields; private final boolean isOpen; public Record(boolean isOpen, Pair<Impl, String>... fields) { this.fields = fields; this.isOpen = isOpen; } @Override public boolean isOpen() { return isOpen; } @Override public int size() { return fields.length; } @Override public int getFieldIndex(String name) { for (int i = 0; i != fields.length; ++i) { String field = fields[i].second(); if (field.equals(name)) { return i; } } throw new IllegalArgumentException("field not found: " + name); } @Override public boolean hasField(String field) { for (int i = 0; i != fields.length; ++i) { if (fields[i].second().equals(field)) { return true; } } return false; } @Override public String[] getFieldNames() { String[] rs = new String[fields.length]; for (int i = 0; i != fields.length; ++i) { rs[i] = fields[i].second(); } return rs; } @Override public Type getReadableFieldType(String field) { return getField(field); } @Override public Type getWriteableFieldType(String field) { return getField(field); } @Override public Type getField(String field) { for (int i = 0; i != fields.length; ++i) { Pair<Impl, String> p = fields[i]; if (p.second().equals(field)) { return p.first(); } } throw new IllegalArgumentException("invalid field " + field); } @Override public Type.Record update(String field, Type type) { // Find field and update (if it exists) Pair<Impl, String>[] nfields = Arrays.copyOf(fields, fields.length); for (int i = 0; i != nfields.length; ++i) { if (nfields[i].second().equals(field)) { // FIXME: this line is clearly broken nfields[i] = new Pair<>((Atom) type, field); return new Impl.Record(isOpen, nfields); } } // If we get here, no match was found. This is an error. throw new IllegalArgumentException("invalid field " + field); } @Override public boolean equals(Object o) { if (o instanceof Impl.Record) { Impl.Record r = (Impl.Record) o; return Arrays.equals(fields, r.fields) && isOpen == r.isOpen; } return false; } @Override public int compareTo(Impl p) { int r = TypeSystem.K_RECORD - p.getKind(); if (r == 0) { Impl.Record n = (Impl.Record) p; if (isOpen != n.isOpen) { return isOpen ? 1 : -1; } else if (fields.length != n.fields.length) { return fields.length - n.fields.length; } for (int i = 0; i != fields.length; ++i) { Pair<Impl, String> p1 = fields[i]; Pair<Impl, String> p2 = n.fields[i]; r = p1.first().compareTo(p2.first()); if (r != 0) { return r; } r = p1.second().compareTo(p2.second()); if (r != 0) { return r; } } return 0; } return r; } @Override public int hashCode() { int f = isOpen ? 1 : 0; return Arrays.hashCode(fields) + f; } @Override public String toString() { String body = ""; boolean firstTime = true; for (int i = 0; i != fields.length; ++i) { Pair<Impl, String> f = fields[i]; if (!firstTime) { body = body + ","; } firstTime = false; body = body + f.first() + " " + f.second(); } if (isOpen) { body += ", ..."; } return "{" + body + "}"; } @Override public int getKind() { return TypeSystem.K_RECORD; } } private static final class Negation extends Atom implements Type.Negation { private final Atom element; private Negation(Atom element) { this.element = element; } @Override public Type element() { return element; } @Override public boolean equals(Object o) { if (o instanceof Impl.Negation) { Impl.Negation n = (Impl.Negation) o; return element.equals(n.element); } return false; } @Override public int compareTo(Impl o) { int r = TypeSystem.K_NEGATION - o.getKind(); if (r == 0) { Impl.Negation n = (Impl.Negation) o; return element.compareTo(n.element); } return r; } @Override public int hashCode() { return -element.hashCode(); } @Override public String toString() { String body = element.toString(); if (element instanceof Array || element instanceof Reference || element instanceof Intersection || element instanceof Union) { return "!(" + element.toString() + ")"; } else { return "!" + body; } } @Override public int getKind() { return TypeSystem.K_NEGATION; } } public abstract static class FunctionOrMethod extends Atom implements Type.FunctionOrMethod { protected final Impl[] parameters; protected final Impl[] returns; public FunctionOrMethod(Impl[] parameters, Impl[] returns) { this.parameters = parameters; this.returns = returns; } /** * Get the return types of this function or method type. * * @return */ @Override public Impl[] returns() { return returns; } /** * Get the parameter types of this function or method type. * * @return */ @Override public Impl[] params() { return parameters; } @Override public Impl parameter(int i) { return parameters[i]; } @Override public boolean equals(Object o) { if (o instanceof Impl.FunctionOrMethod) { Impl.FunctionOrMethod f = (Impl.FunctionOrMethod) o; return Arrays.equals(parameters, f.parameters) && Arrays.equals(returns, f.returns); } return false; } @Override public int compareTo(Impl o) { int r = getKind() - o.getKind(); if (r == 0) { Impl.FunctionOrMethod fm = (Impl.FunctionOrMethod) o; r = ArrayUtils.compareTo(parameters, fm.parameters); if (r == 0) { r = ArrayUtils.compareTo(returns, fm.returns); } } return r; } @Override public int hashCode() { int hc = Arrays.hashCode(parameters); hc += Arrays.hashCode(returns); return hc; } } /** * A function type, consisting of a list of zero or more parameters and * a return type. * * @author David J. Pearce * */ public static class Function extends FunctionOrMethod implements Type.Function { public Function(Impl[] parameters, Impl[] returns) { super(parameters, returns); } @Override public String toString() { String ps = Impl.toString(parameters); String rs = Impl.toString(returns); return "function(" + ps + ")->(" + rs + ")"; } @Override public int getKind() { return TypeSystem.K_FUNCTION; } } public static final class Method extends FunctionOrMethod implements Type.Method { protected final String[] lifetimeParameters; protected final String[] contextLifetimes; public Method(Impl[] parameters, Impl[] returns) { super(parameters, returns); this.lifetimeParameters = new String[0]; this.contextLifetimes = new String[0]; } public Method(String[] lifetimeParameters, String[] contextLifetimes, Impl[] parameters, Impl[] returns) { super(parameters, returns); this.lifetimeParameters = lifetimeParameters; this.contextLifetimes = contextLifetimes; } public Method(Collection<String> lifetimeParameters, Collection<String> contextLifetimes, Impl[] parameters, Impl[] returns) { super(parameters, returns); this.lifetimeParameters = ArrayUtils.toStringArray(lifetimeParameters); this.contextLifetimes = ArrayUtils.toStringArray(contextLifetimes); } @Override public boolean equals(Object o) { if (o instanceof Impl.Method) { Impl.Method m = (Impl.Method) o; return Arrays.equals(lifetimeParameters, m.lifetimeParameters) && Arrays.equals(contextLifetimes, m.contextLifetimes) && super.equals(o); } return false; } @Override public int hashCode() { int hc = super.hashCode(); hc += Arrays.hashCode(lifetimeParameters); hc += Arrays.hashCode(contextLifetimes); return hc; } /** * Get the context lifetimes of this function or method type. * * @return */ @Override public String[] contextLifetimes() { return contextLifetimes; } /** * Get the lifetime parameters of this function or method type. * * @return */ @Override public String[] lifetimeParams() { return lifetimeParameters; } @Override public String toString() { String prefix = "method"; if (contextLifetimes.length > 0) { prefix += "[" + Impl.<String>toString(contextLifetimes) + "]"; } if (lifetimeParameters.length > 0) { prefix += "<" + Impl.<String>toString(lifetimeParameters) + ">"; } String ps = Impl.<Impl>toString(parameters); String rs = Impl.<Impl>toString(returns); return prefix + "(" + ps + ")->(" + rs + ")"; } @Override public int getKind() { return TypeSystem.K_METHOD; } } public static final class Property extends FunctionOrMethod implements Type.Property { public Property(Impl[] parameters) { super(parameters, new Impl[]{(Impl)T_BOOL}); } @Override public String toString() { String ps = Impl.toString(parameters); return "property(" + ps + ")"; } @Override public int getKind() { return TypeSystem.K_PROPERTY; } } private static class Conjunct extends Conjunctable implements Intersection { private final Impl.Atom[] atoms; private Conjunct(Impl.Atom... atoms) { this.atoms = atoms; } /** * Return the bounds of this union type. * * @return */ @Override public Type[] bounds() { return atoms; } @Override public int getKind() { return TypeSystem.K_INTERSECTION; } @Override public boolean equals(Object o) { if (o instanceof Impl.Conjunct) { Impl.Conjunct r = (Impl.Conjunct) o; return Arrays.equals(atoms, r.atoms); } return false; } @Override public int compareTo(Impl p) { int r = TypeSystem.K_INTERSECTION - p.getKind(); if (r == 0) { Impl.Conjunct n = (Impl.Conjunct) p; r = ArrayUtils.compareTo(atoms, n.atoms); } return r; } @Override public int hashCode() { return Arrays.hashCode(atoms); } @Override public String toString() { String body = ""; for (int i = 0; i != atoms.length; ++i) { Type element = atoms[i]; if (i != 0) { body = body + "&"; } if (element instanceof Union || element instanceof Reference) { body = body + "(" + element + ")"; } else { body = body + element; } } return body; } } private static abstract class Union<T extends Impl> extends Impl implements Type.Union { // INVARIANT: elements in sorted order // INVARIANT: elements > 1 protected final T[] elements; private Union(T... elements) { this.elements = elements; } /** * Return the bounds of this union type. * * @return */ @Override public T[] bounds() { return elements; } @Override public boolean equals(Object o) { if (o instanceof Impl.Union) { Impl.Union r = (Impl.Union) o; return Arrays.equals(elements, r.elements); } return false; } @Override public int compareTo(Impl p) { int r = TypeSystem.K_UNION - p.getKind(); if (r == 0) { Impl.Union<?> n = (Impl.Union<?>) p; if (elements.length != n.elements.length) { return elements.length - n.elements.length; } for (int i = 0; i != elements.length; ++i) { Impl p1 = elements[i]; Impl p2 = n.elements[i]; r = p1.compareTo(p2); if (r != 0) { return r; } } return 0; } return r; } @Override public int hashCode() { return Arrays.hashCode(elements); } @Override public String toString() { String body = ""; for (int i = 0; i != elements.length; ++i) { Type element = elements[i]; if (i != 0) { body = body + "|"; } if (element instanceof Intersection || element instanceof Reference) { body = body + "(" + element + ")"; } else { body = body + element; } } return body; } @Override public int getKind() { return TypeSystem.K_UNION; } } private static class UnionOfConjunctables extends Union<Conjunctable> { public UnionOfConjunctables(Conjunctable... conjunctables) { super(conjunctables); } } /** * A union of records corresponds to an <i>effective record</i> in * Whiley. This means it can be treated in a similar fashion to a record * in many situations. * * @author David J. Pearce * */ private static class UnionOfRecords extends Union<Impl.Record> implements EffectiveRecord { public UnionOfRecords(Impl.Record... records) { super(records); if (records.length < 2) { throw new IllegalArgumentException("insufficient records provided"); } } @Override public int size() { return getFieldNames().length; } @Override public int getFieldIndex(String field) { // FIXME: it's not really clear what this method should do in // this circumstance. int i = ArrayUtils.firstIndexOf(getFieldNames(), field); if (i < 0) { throw new IllegalArgumentException("field not found: " + field); } else { return i; } } @Override public boolean hasField(String field) { for (int i = 0; i != elements.length; ++i) { if (!elements[i].hasField(field)) { return false; } } return true; } @Override public String[] getFieldNames() { // To compute the set of fields accessible from a union of // records, we must intersect the the set of available fields in // each nested record. String[] first = elements[0].getFieldNames(); String[] result = new String[first.length]; outer: for (int i = 0; i != result.length; ++i) { // FIXME: this is a slight cheat, since it's only computing // the common initial sequence for fields. At this stage, // it's unclear whether or not this is sufficient. In // particular, whether or not the language design should // permit more flexibility here. String field = first[i]; for (int j = 0; j != elements.length; ++j) { if (!elements[j].hasField(field)) { break outer; } } result[i] = field; } return result; } @Override public Type getReadableFieldType(String field) { Type readable = elements[0].getField(field); for (int i = 1; i != elements.length; ++i) { readable = Union(readable, elements[i].getField(field)); } return readable; } @Override public Type getWriteableFieldType(String field) { Type readable = elements[0].getField(field); for (int i = 1; i != elements.length; ++i) { readable = Intersection(readable, elements[i].getField(field)); } return readable; } @Override public EffectiveRecord update(String field, Type type) { Impl.Record[] records = new Impl.Record[elements.length]; for (int i = 0; i != elements.length; ++i) { records[i] = (Impl.Record) elements[i].update(field, type); } return (EffectiveRecord) Union(records); } } /** * A union of arrays corresponds to an <i>effective array</i> in Whiley. * This means it can be treated in a similar fashion to a array in many * situations. * * @author David J. Pearce * */ private static class UnionOfArrays extends Union<Impl.Array> implements EffectiveArray { public UnionOfArrays(Impl.Array... arrays) { super(arrays); if (arrays.length < 2) { throw new IllegalArgumentException("insufficient arrays provided"); } } @Override public Type getReadableElementType() { Type readable = elements[0].element(); for (int i = 1; i != elements.length; ++i) { readable = Union(readable, elements[i].element()); } return readable; } @Override public Type getWriteableElementType() { Type readable = elements[0].element(); for (int i = 1; i != elements.length; ++i) { readable = Intersection(readable, elements[i].element()); } return readable; } @Override public EffectiveArray update(Type type) { Impl.Array[] arrays = new Impl.Array[elements.length]; for (int i = 0; i != elements.length; ++i) { arrays[i] = elements[i].update(type); } return (EffectiveArray) Union(arrays); } } private static <T> String toString(T[] things) { String body = ""; boolean firstTime = true; for (T thing : things) { if (!firstTime) { body = body + ","; } firstTime = false; body = body + thing; } return body; } // ============================================================= // Simplifications // ============================================================= /** * Identify the common kind (if any) amongst a sequence of two or more * types. * * @param cs * @return */ static int determineCommonKind(Impl.Conjunctable... cs) { int kind = cs[0].getKind(); for (int i = 1; i != cs.length; ++i) { if (cs[i].getKind() != kind) { return -1; } } return kind; } /** * Flatten a sequence of zero or more types into a sequence of zero or * more conjunctables.union types to form one big array of types. The * resulting sequence may be larger than the original sequence. For * example, if the original sequence contains a union of some sort. * * @param types * @return */ private static Conjunctable[] toConjunctables(Type... types) { int length = 0; for (int i = 0; i != types.length; ++i) { Type ith = types[i]; if (ith instanceof Union) { Union ut = (Union) ith; length = length + ut.bounds().length; } else { length = length + 1; } } Conjunctable[] nTypes = new Conjunctable[length]; for (int i = 0, j = 0; i != types.length; ++i) { Type ith = types[i]; if (ith instanceof Union) { Union ut = (Union) ith; Type[] ut_bounds = ut.bounds(); System.arraycopy(ut_bounds, 0, nTypes, j, ut_bounds.length); j += ut_bounds.length; } else { nTypes[j++] = (Conjunctable) ith; } } return nTypes; } /** * Flattern union types to form one big array of types. * * @param types * @return */ private static Atom[] toAtoms(Type... types) { int length = 0; for (int i = 0; i != types.length; ++i) { Type ith = types[i]; if (ith instanceof Intersection) { Intersection it = (Intersection) ith; length = length + it.bounds().length; } else { length = length + 1; } } // Atom[] nTypes = new Atom[length]; for (int i = 0, j = 0; i != types.length; ++i) { Type ith = types[i]; if (ith instanceof Intersection) { // This indicates a nested intersection. This needs to // be flatterned out, since an intersection is not an // atom. Intersection it = (Intersection) ith; Type[] ut_bounds = it.bounds(); System.arraycopy(ut_bounds, 0, nTypes, j, ut_bounds.length); j += ut_bounds.length; } else { // At this stage, we can assume ith is not a union. // This is because unions are previously removed early // in Intersection by distributing over them. nTypes[j++] = (Atom) ith; } } return nTypes; } /** * Determine the first index of a union within the sequence of types, or * -1 if none. This is used to find the distribution point within a * given set of atoms being intersected. * * @param types * @return */ private static int findUnion(Type... types) { for (int i = 0; i != types.length; ++i) { Type ith = types[i]; if (ith instanceof Union) { return i; } } // We didn't find any unions return -1; } /** * Apply the laws of distributivity for a sequence of one or more types * and a given union within that sequence. Specifically, for any nested * unions, we must extract an distribute over them. This is a relatively * expensive operation, and the performance of the algorithm implemented * here could be vastly improved with some care. * * @param types * @return */ private static Type distributeUnion(int unionIndex, Type... types) { Union<?> ut = (Union<?>) types[unionIndex]; Type[] ut_bounds = ut.bounds(); // Distribute over the union Type[] clauses = new Type[ut_bounds.length]; for (int j = 0; j != ut_bounds.length; ++j) { Type jth = ut_bounds[j]; Type[] tmp = Arrays.copyOf(types, types.length); tmp[unionIndex] = jth; clauses[j] = Intersection(tmp); } return Union(clauses); } /** * <p> * Intersect two types which are atoms (and not null). That is, they are * neither unions nor intersections. In many cases, either the two types * are the same or no intersection is possible. In some cases (e.g. for * records and arrays), we must recursively intersect the element types * to decide whether an intersection is possible. * </p> * <p> * As part of the intersection process, the algorithm will combine types * where appropriate. For example, duplicates are combined into tone. In * such case, the result is stored in the ith position and the jth * position is set to null. * </p> * <p> * Finally, if the algorithm determines the overall type is void then it * will stop early and return false to signal a failure. * </p> * * @param i * ith index to consider * @param j * jth index to consider * @param types * Array of types being intersected */ private static void intersectAtoms(int i, int j, Atom[] types) { // System.out.println("INTERSECTING: " + i + ", " + j + " in " + // Arrays.toString(types)); Atom ith = types[i]; Atom jth = types[j]; int ith_kind = ith.getKind(); int jth_kind = jth.getKind(); if (ith_kind == TypeSystem.K_NEGATION && jth_kind == TypeSystem.K_NEGATION) { // Intersection of negative atoms intersectNegativeNegative(i, j, types); } else if (ith_kind == TypeSystem.K_NEGATION) { // Intersection of negative with positive intersectNegativePositive(i, j, types); } else if (jth_kind == TypeSystem.K_NEGATION) { // Intersection of positive with negative intersectNegativePositive(j, i, types); } else { // Intersection of positive with positive intersectPositivePositive(i, j, types); } } private static void intersectNegativeNegative(int i, int j, Atom[] types) { // FIXME: Unsure what could do here. } private static void intersectNegativePositive(int i, int j, Atom[] types) { Negation ith = (Negation) types[i]; Impl.Atom ith_element = ith.element; Atom jth = types[j]; if(jth == T_ANY) { // Do nothing as any will be dropped eventually. } else if(ith_element.equals(jth)) { // FIXME: should do more here as there are other cases where we // should reduce to void. For example, if jth element is // supertype of ith. types[i] = (Atom) T_VOID; types[j] = (Atom) T_VOID; } else if(ith_element instanceof Nominal || jth instanceof Nominal) { // There's not much we can do here, since we can't be sure // whether or not the Nominal types having anything in common. } else { // ith is subsumed types[i] = (Atom) T_ANY; } } private static void intersectPositivePositive(int i, int j, Atom[] types) { Atom ith = types[i]; Atom jth = types[j]; // int ith_kind = ith.getKind(); int jth_kind = jth.getKind(); // if (ith_kind == TypeSystem.K_ANY || jth_kind == TypeSystem.K_ANY) { // In this case, there is nothing really to do. Basically // intersection something with any gives something. } else if (ith_kind == TypeSystem.K_NOMINAL || jth_kind == TypeSystem.K_NOMINAL) { // In this case, there is also nothing to do. That's because // we don't know what a nominal is, and hence we are // essentially treating it as being the same as any. } else if (ith_kind != jth_kind) { // There are no situations where this results in a positive // outcome. Therefore, set both parties to be void. This // guarantees the void will be caught at the earliest possible // moment, types[i] = (Atom) T_VOID; types[j] = (Atom) T_VOID; } else { // Now check individual cases switch (ith_kind) { case TypeSystem.K_VOID: case TypeSystem.K_BOOL: case TypeSystem.K_BYTE: case TypeSystem.K_META: case TypeSystem.K_INT: // For primitives, it's enough to know that they have // the same kind. break; case TypeSystem.K_ARRAY: { // For arrays, we need to know whether or not their element // types intersect. types[i] = (Atom) intersectArray((Impl.Array) types[i], (Impl.Array) types[j]); types[j] = (Atom) T_ANY; break; } case TypeSystem.K_RECORD: { // For records, we need to know whether their fields match // appropriately and whether or not they intersect. types[i] = (Atom) intersectRecord((Impl.Record) types[i], (Impl.Record) types[j]); types[j] = (Atom) T_ANY; break; } case TypeSystem.K_REFERENCE: { // For arrays, we need to know whether or not their element // types match exactly. types[i] = (Atom) intersectReference((Impl.Reference) types[i], (Impl.Reference) types[j]); types[j] = (Atom) T_ANY; break; } case TypeSystem.K_FUNCTION: case TypeSystem.K_METHOD: default: throw new RuntimeException("DEADCODE REACHED"); } } } /** * <p> * Intersect one record type with another. The computation is fairly * straight-forward. For every field in common, we intersect their * types. For example, <code>{int|null f}&{int|bool f}</code> gives * <code>{int f}</code> because <code>(int|null)&(int|bool)</code> gives * <code>int</code>. * </p> * * <p> * In the case that there are fields in one but not the other, the * result is void. The only exception is when one or both of the records * is open. When both records are open, there is always an intersection. * When only one is open, then it's fields must be a subset of the * other. * </p> * * @param ith * First record type to intersect * @param jth * Second record type to intersect */ private static Type intersectRecord(Impl.Record ith, Impl.Record jth) { if (ith.isOpen && jth.isOpen) { return intersectOpenOpenRecord(ith, jth); } else if (ith.isOpen) { return intersectOpenClosedRecord(ith, jth); } else if (jth.isOpen) { return intersectOpenClosedRecord(jth, ith); } else { return intersectClosedClosedRecord(jth, ith); } } /** * When intersecting two closed records, we require they both have the * same set of fields. Then we simply intersect each respective field. * * @param ith * @param jth * @return */ public static Type intersectClosedClosedRecord(Impl.Record ith, Impl.Record jth) { // ith and jth fields should be in sorted order by field name Pair<Impl, String>[] ith_fields = ith.fields; Pair<Impl, String>[] jth_fields = jth.fields; // Sanity check the lengths if (ith_fields.length != jth_fields.length) { // Since the lengths are different, we know these records cannot // be intersected. return T_VOID; } else { // Lengths are equal, so check fields match and intersect them. Pair<Type, String>[] new_fields = new Pair[ith_fields.length]; for (int i = 0; i != ith_fields.length; ++i) { Pair<Impl, String> ith_field = ith_fields[i]; Pair<Impl, String> jth_field = jth_fields[i]; String ith_name = ith_field.second(); String jth_name = jth_field.second(); if (ith_name.equals(jth_name)) { Type type = Intersection(ith_field.first(), jth_field.first()); new_fields[i] = new Pair<>(type, ith_name); } else { // We've found a field that is not comon to both. return T_VOID; } } return Type.Record(false, new_fields); } } /** * When intersecting an open record with a closed record. In this case, * the result is void if there is a field in the open record not present * in the closed record. Otherwise, the result is the intersection of * those fields common to both with the remainder taken from the closed * record. * * @param open * @param jth * @return */ public static Type intersectOpenClosedRecord(Impl.Record open, Impl.Record closed) { // ith and jth fields should be in sorted order by field name Pair<Impl, String>[] open_fields = open.fields; Pair<Impl, String>[] closed_fields = closed.fields; // Sanity check the lengths if (open_fields.length > closed_fields.length) { // In this case, it is impossible for those fields in the open // record to be a subset of those in the closed record. return T_VOID; } else { // The aim now is to // Lengths are equal, so check fields match and intersect them. Pair<Type, String>[] new_fields = new Pair[closed_fields.length]; for (int i = 0, j = 0; i != new_fields.length; ++i) { Pair<Impl, String> closed_field = closed_fields[i]; String closed_name = closed_field.second(); if (j < open_fields.length) { Pair<Impl, String> open_field = open_fields[j]; String open_name = open_field.second(); int c = open_name.compareTo(closed_name); if (c > 0) { // In this case, there is a field in the closed // record not present in the open record. new_fields[i] = (Pair) closed_field; } else if (c == 0) { // In this case, there is a field common to both the // closed and open records. Type type = Intersection(open_field.first(), closed_field.first()); new_fields[i] = new Pair<>(type, open_name); j = j + 1; } else { // In this case, there is a field in the open record // which is not in the closed record. Hence, no // intersection is possible. return T_VOID; } } else { // In this case, there is a field in the closed record // not present in the open record. new_fields[i] = (Pair) closed_fields[i]; } } return Type.Record(false, new_fields); } } /** * When intersecting an open record with another open record, there is * always a resulting intersection. In this case, fields common to both * are intersected and the remainder are pulled as is. * * @param ith * @param jth * @return */ public static Type intersectOpenOpenRecord(Impl.Record ith, Impl.Record jth) { // ith and jth fields should be in sorted order by field name Pair<Impl, String>[] ith_fields = ith.fields; Pair<Impl, String>[] jth_fields = jth.fields; ArrayList<Pair<Type, String>> new_fields = new ArrayList<>(); int i = 0; int j = 0; while (i < ith_fields.length && j < jth_fields.length) { Pair<Impl, String> ith_field = ith_fields[i]; Pair<Impl, String> jth_field = jth_fields[j]; String ith_name = ith_field.second(); String jth_name = jth_field.second(); int c = ith_name.compareTo(jth_name); if (c < 0) { // In this case, there is a field in the ith // record not present in the jth record. new_fields.add((Pair) ith_fields[i]); i = i + 1; } else if (c == 0) { // In this case, there is a field common to both the // closed and open records. Type type = Intersection(ith_field.first(), jth_field.first()); new_fields.add(new Pair<>(type, ith_name)); i = i + 1; j = j + 1; } else { // In this case, there is a field in the jth // record not present in the ith record. new_fields.add((Pair) jth_fields[i]); j = j + 1; } } // Finally, tidy up any lose ends for (; i < ith_fields.length; i = i + 1) { new_fields.add((Pair) ith_fields[i]); } for (; j < jth_fields.length; j = j + 1) { new_fields.add((Pair) jth_fields[j]); } // Done return Type.Record(false, new_fields); } /** * The intersection of two arrays is simply the intersection of their * element types. Observe that if this is void, then the resulting type * will be void as well. * * @param ith * @param jth * @return */ private static Type intersectArray(Type.Array ith, Type.Array jth) { Type element = Intersection(ith.element(), jth.element()); return Type.Array(element); } /** * The intersection of two references is only possible if their type * match exactly. * * @param ith * @param jth * @return */ private static Type intersectReference(Type.Reference ith, Type.Reference jth) { if(ith.element().equals(jth.element())) { return ith; } else { return Type.T_VOID; } } } }