// 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.util.type; import java.util.*; import wyautl_old.lang.*; import wybs.lang.NameID; import wyil.lang.Type; import wyil.util.TypeSystem; /** * <p> * The subtype operator implements the algorithm for determining whether or not * one type is a <i>subtype</i> of another. For the most part, one can take * subtype to mean <i>subset</i> (this analogy breaks down with function types, * however). Following this analogy, <code>T1</code> is a subtype of * <code>T2</code> (denoted <code>T1 <: T2</code>) if the set of values * represented by <code>T1</code> is a subset of those represented by * <code>T2</code>. * </p> * <p> * The algorithm actually operates by computing the <i>intersection</i> relation * for two types (i.e. whether or not an intersection exists between their set * of values). Subtyping is closely related to intersection and, in fact, we * have that <code>T1 :> T2</code> iff <code>!(!T1 & T2)</code> (where * <code>&</code> is the intersection relation). The choice to compute * intersections, rather than subtypes, was for simplicity. Namely, it was * considered conceptually easier to think about intersections rather than * subtypes. * </p> * <p> * <b>NOTE:</b> for this algorithm to return correct results in all cases, both * types must have been normalised first. * </p> * <h3>References</h3> * <ul> * <li><p>David J. Pearce and James Noble. Structural and Flow-Sensitive Types for * Whiley. Technical Report, Victoria University of Wellington, 2010.</p></li> * <li><p>A. Frisch, G. Castagna, and V. Benzaken. Semantic subtyping. In * Proceedings of the <i>Symposium on Logic in Computer Science</i>, pages * 137--146. IEEE Computer Society Press, 2002.</p></li> * <li><p>Dexter Kozen, Jens Palsberg, and Michael I. Schwartzbach. Efficient * recursive subtyping. In <i>Proceedings of the ACM Conference on Principles of * Programming Languages</i>, pages 419--428, 1993.</p></li> * <li><p>Roberto M. Amadio and Luca Cardelli. Subtyping recursive types. <i>ACM * Transactions on Programming Languages and Systems</i>, * 15:575--631, 1993.</p></li> * </ul> * * @author David J. Pearce * */ public class SubtypeOperator { protected final Automaton from; protected final Automaton to; private final Map<String, String> fromLifetimeSubstitution; private final Map<String, String> toLifetimeSubstitution; private final LifetimeRelation lifetimeRelation; private final BitSet assumptions; public SubtypeOperator(Automaton from, Automaton to, LifetimeRelation lr) { this.from = from; this.to = to; this.lifetimeRelation = lr; this.fromLifetimeSubstitution = Collections.emptyMap(); this.toLifetimeSubstitution = Collections.emptyMap(); // matrix is twice the size to accommodate positive and negative signs this.assumptions = new BitSet((2*from.size()) * (to.size()*2)); //System.out.println("FROM: " + from); //System.out.println("TO: " + to); } private SubtypeOperator(SubtypeOperator sto, Map<String, String> fromLifetimeSubstitution, Map<String, String> toLifetimeSubstitution) { this.from = sto.from; this.to = sto.to; this.fromLifetimeSubstitution = new HashMap<String, String>(sto.fromLifetimeSubstitution); prependLifetimes(this.fromLifetimeSubstitution); this.fromLifetimeSubstitution.putAll(fromLifetimeSubstitution); this.toLifetimeSubstitution = new HashMap<String, String>(sto.toLifetimeSubstitution); prependLifetimes(this.toLifetimeSubstitution); this.toLifetimeSubstitution.putAll(toLifetimeSubstitution); this.lifetimeRelation = sto.lifetimeRelation; this.assumptions = sto.assumptions; } /** * Test whether <code>from</code> :> <code>to</code> * @param fromIndex * @param toIndex * @return */ public final boolean isSubtype(int fromIndex, int toIndex) { return !isIntersection(fromIndex,false,toIndex,true); } /** * Test whether <code>from</code> <: <code>to</code> * @param fromIndex * @param toIndex * @return */ public final boolean isSupertype(int fromIndex, int toIndex) { return !isIntersection(fromIndex,true,toIndex,false); } /** * Determine whether there is a non-empty intersection between the state * rooted at <code>fromIndex</code> and that rooted at <code>toIndex</code>. * The signs indicate whether or not the state should be taken as its * <i>inverse</i>. * * @param fromIndex * --- index of from state * @param fromSign * --- sign of from state (true = normal, false = inverted). * @param toIndex * --- index of to state * @param toSign * --- sign of from state (true = normal, false = inverted). * @return --- true if such an intersection exists, false otherwise. */ protected boolean isIntersection(int fromIndex, boolean fromSign, int toIndex, boolean toSign) { //System.out.println("STARTING: " + fromIndex + "(" + fromSign + ") & " + toIndex + "(" + toSign + ")"); // TODO: can further improve performance using caching int index = indexOf(fromIndex,fromSign,toIndex,toSign); if(assumptions.get(index)) { //System.out.println("ASSUMED: " + fromIndex + "(" + fromSign + ") & " + toIndex + "(" + toSign + ")"); return false; } else { assumptions.set(index,true); } boolean r = isIntersectionInner(fromIndex,fromSign,toIndex,toSign); assumptions.set(index,false); //System.out.println("RESULT: " + fromIndex + "(" + fromSign + ") & " + toIndex + "(" + toSign + ") = " + r); return r; } protected boolean isIntersectionInner(int fromIndex, boolean fromSign, int toIndex, boolean toSign) { Automaton.State fromState = from.states[fromIndex]; Automaton.State toState = to.states[toIndex]; int fromKind = fromState.kind; int toKind = toState.kind; if(fromKind == toKind) { switch(fromKind) { case TypeSystem.K_VOID: return !fromSign && !toSign; case TypeSystem.K_ANY: return fromSign && toSign; // === Leaf States First === case TypeSystem.K_NOMINAL: { NameID nid1 = (NameID) fromState.data; NameID nid2 = (NameID) toState.data; if(fromSign || toSign) { if(nid1.equals(nid2)) { return fromSign && toSign; } else { return !fromSign || !toSign; } } return true; } // === Homogenous Compound States === case TypeSystem.K_ARRAY: // != below not ||. This is because lists and sets can intersect // on the empty list/set. if(fromSign != toSign) { // nary nodes int fromChild = fromState.children[0]; int toChild = toState.children[0]; if (!isIntersection(fromChild, fromSign, toChild, toSign)) { return false; } } return true; case TypeSystem.K_REFERENCE: if(fromSign || toSign) { int fromChild = fromState.children[0]; int toChild = toState.children[0]; if (fromSign && toSign) { // "Is there any value in both &a:A and &b:B ?" // // This is equivalent to the question "Is there any value in both &A and &B?" // If there is a value in both &a:A and &b:B, then we can take that value // and change its lifetime to *. It will then be in both &A and &B. // If there is a value in both &A and &B, then it is also in both &a:A and &b:B // because the lifetime * outlives a and b. // // Finally, there is a value in both &A and &B iff the types A and B intersect. return isIntersection(fromChild, true, toChild, true); } else { String fromLifetime = applyFromLifetimeSubstitution((String) fromState.data); String toLifetime = applyToLifetimeSubstitution((String) toState.data); // if (fromSign) { // A & !B ==> !!(A & !B) // &from:T1 & !(&to:T2) ==> not (&from:T1) <: (&to:T2). return !( // &from:T1 is a subtype of &to:T2 iff all these are true: // from outlives to lifetimeRelation.outlives(fromLifetime, toLifetime) // T1 is a subtype of T2 && !isIntersection(fromChild, true, toChild, false) // T2 is a subtype of T1 && !isIntersection(fromChild, false, toChild, true) ); } else { // not (&to:T1) <: (&from:T2). return !( // &to:T1 is a subtype of &from:T2 iff all these are true: // to outlives from lifetimeRelation.outlives(toLifetime, fromLifetime) // T2 is a subtype of T1 && !isIntersection(fromChild, false, toChild, true) // T1 is a subtype of T2 && !isIntersection(fromChild, true, toChild, false) ); } } } // An inverted reference type always contains the value "null". // Both are inverted, so they intersect with "null". return true; case TypeSystem.K_RECORD: return intersectRecords(fromIndex,fromSign,toIndex,toSign); case TypeSystem.K_NEGATION: case TypeSystem.K_UNION : // let these cases fall through to if-statements after // switch. break; // === Heterogenous Compound States === case TypeSystem.K_FUNCTION: case TypeSystem.K_METHOD: if(fromSign || toSign) { // nary nodes int[] fromChildren = fromState.children; int[] toChildren = toState.children; TypeSystem.FunctionOrMethodState fromData = (TypeSystem.FunctionOrMethodState) fromState.data; TypeSystem.FunctionOrMethodState toData = (TypeSystem.FunctionOrMethodState) toState.data; int fromNumParams = fromData.numParams; int toNumParams = toData.numParams; List<String> fromLifetimeParameters = fromData.lifetimeParameters; List<String> toLifetimeParameters = toData.lifetimeParameters; if (fromSign && toSign) { // Two intersecting method types must have the same number // of parameters, returns and lifetime parameters. if (fromChildren.length != toChildren.length || fromNumParams != toNumParams || fromLifetimeParameters.size() != toLifetimeParameters.size()){ return false; } // Context lifetimes do not matter, because we can always choose // a method without context lifetimes to find an intersecting method. } else { // Now we have one of the following: // fromSign == true: negation of "is fromType a subtype of toType?" // toSign == true: negation of "is toType a subtype of fromType?" // Two method types can only be subtypes of each other if they have // the same number of parameters, returns and lifetime parameters. if (fromChildren.length != toChildren.length || fromNumParams != toNumParams || fromLifetimeParameters.size() != toLifetimeParameters.size()){ return true; // true means "not subtype" } // The supertype must contain all context lifetimes of the subtype List<String> fromContextLifetimes = applyFromLifetimeSubstitution(fromData.contextLifetimes); List<String> toContextLifetimes = applyToLifetimeSubstitution(toData.contextLifetimes); if (fromSign) { if (!toContextLifetimes.containsAll(fromContextLifetimes)) { return true; } } else { if (!fromContextLifetimes.containsAll(toContextLifetimes)) { return true; } } } // Calculate the substitutions for lifetime parameters Map<String, String> fromLifetimeSubstitution = buildLifetimeSubstitution(fromLifetimeParameters); Map<String, String> toLifetimeSubstitution = buildLifetimeSubstitution(toLifetimeParameters); SubtypeOperator sto = this; if (!fromLifetimeSubstitution.isEmpty() || !toLifetimeSubstitution.isEmpty()) { sto = new SubtypeOperator(this, fromLifetimeSubstitution, toLifetimeSubstitution); } if (fromSign && toSign) { // Two intersecting method types must have intersecting return types. // Parameter types can always be chosen as "any". for (int i = fromNumParams; i < fromChildren.length; ++i) { if (!sto.isIntersection(fromChildren[i], true, toChildren[i], true)) { return false; } } return true; } // here we have the subtype test again... // Parameter types are contra-variant for (int i = 0; i < fromNumParams; ++i) { if (sto.isIntersection(fromChildren[i], !fromSign, toChildren[i], !toSign)) { return true; } } // Return types are co-variant for (int i = fromNumParams; i < fromChildren.length; ++i) { if (sto.isIntersection(fromChildren[i], fromSign, toChildren[i], toSign)) { return true; } } return false; } return true; default: return fromSign == toSign; } } if(fromKind == TypeSystem.K_NEGATION) { int fromChild = fromState.children[0]; return isIntersection(fromChild,!fromSign,toIndex,toSign); } else if(toKind == TypeSystem.K_NEGATION) { int toChild = toState.children[0]; return isIntersection(fromIndex,fromSign,toChild,!toSign); } // using invert helps reduce the number of cases to consider. fromKind = invert(fromKind,fromSign); toKind = invert(toKind,toSign); if(fromKind == TypeSystem.K_VOID || toKind == TypeSystem.K_VOID){ return false; } else if(fromKind == TypeSystem.K_UNION) { int[] fromChildren = fromState.children; for(int i : fromChildren) { if(isIntersection(i,fromSign,toIndex,toSign)) { return true; } } return false; } else if(toKind == TypeSystem.K_UNION) { int[] toChildren = toState.children; for(int j : toChildren) { if(isIntersection(fromIndex,fromSign,j,toSign)) { return true; } } return false; } else if(fromKind == TypeSystem.K_INTERSECTION) { int[] fromChildren = fromState.children; for (int i : fromChildren) { if(!isIntersection(i,fromSign,toIndex,toSign)) { return false; } } return true; } else if(toKind == TypeSystem.K_INTERSECTION) { int[] toChildren = toState.children; for (int j : toChildren) { if(!isIntersection(fromIndex,fromSign,j,toSign)) { return false; } } return true; } else if(fromKind == TypeSystem.K_ANY || toKind == TypeSystem.K_ANY){ return true; } return !fromSign || !toSign; } private static Map<String, String> buildLifetimeSubstitution(List<String> lifetimeParameters) { if (lifetimeParameters.isEmpty()) { return Collections.emptyMap(); } Map<String, String> substitution = new HashMap<String, String>(); int i = 0; for (String lifetime : lifetimeParameters) { substitution.put(lifetime, "$" + (i++)); } return substitution; } private static void prependLifetimes(Map<String, String> substitution) { for (Map.Entry<String, String> entry : substitution.entrySet()) { entry.setValue("$" + entry.getValue()); } } private String applyFromLifetimeSubstitution(String lifetime) { String replacement = fromLifetimeSubstitution.get(lifetime); if (replacement != null) { return replacement; } return lifetime; } private String applyToLifetimeSubstitution(String lifetime) { String replacement = toLifetimeSubstitution.get(lifetime); if (replacement != null) { return replacement; } return lifetime; } private List<String> applyFromLifetimeSubstitution(List<String> lifetimes) { if (lifetimes.isEmpty() || fromLifetimeSubstitution.isEmpty()) { return lifetimes; } List<String> replacement = new ArrayList<String>(lifetimes.size()); for (String lifetime : lifetimes) { replacement.add(applyFromLifetimeSubstitution(lifetime)); } return replacement; } private List<String> applyToLifetimeSubstitution(List<String> lifetimes) { if (lifetimes.isEmpty() || toLifetimeSubstitution.isEmpty()) { return lifetimes; } List<String> replacement = new ArrayList<String>(lifetimes.size()); for (String lifetime : lifetimes) { replacement.add(applyToLifetimeSubstitution(lifetime)); } return replacement; } /** * <p> * Check for intersection between two states with kind K_RECORD. The * distinction between open and closed records adds complexity here. * </p> * * <p> * Intersection between <b>closed</b> records is the easiest case. The main * examples are: * </p> * <ul> * <li><code>{T1 f, T2 g} & {T3 f, T4 g} = if T1&T3 and T2&T4</code>.</li> * <li><code>{T1 f, T2 g} & {T3 f, T4 h} = false</code>.</li> * <li><code>{T1 f, T2 g} & !{T3 f, T4 g} = if T1&!T3 or T2&!T4</code>.</li> * <li><code>{T1 f, T2 g} & !{T3 f} = false</code>.</li> * <li><code>!{T1 f} & !{T2 f} = true</code>.</li> * </ul> * <p> * Intersection between a <b>closed</b> and <b>open</b> record is similar. The main examples are: * </p> * <ul> * <li><code>{T1 f, T2 g, ...} & {T3 f, T4 g} = if T1&T3 and T2&T4</code>.</li> * <li><code>{T1 f, ...} & {T2 f, T3 g} = if T1&T2</code>.</li> * <li><code>{T1 f, T2 g, ...} & {T3 f, T4 h} = false</code>.</li> * <li><code>{T1 f, T2 g, ...} & !{T3 f, T4 g} = if T1&!T3 or T2&!T4</code>.</li> * * <li><code>!{T1 f, T2 g, ...} & {T3 f, T4 g} = if T1&!T3 or T2&!T4</code>.</li> * * <li><code>{T1 f, ...} & !{T2 f, T3 g} = true</code>.</li> * <li><code>{T1 f, T2 g, ...} & !{T3 f, T4 h} = false</code>.</li> * <li><code>!{T1 f,...} & !{T2 f} = true</code>.</li> * </ul> * * @param fromIndex * --- index of from state * @param fromSign * --- sign of from state (true = normal, false = inverted). * @param toIndex * --- index of to state * @param toSign * --- sign of from state (true = normal, false = inverted). * @return --- true if such an intersection exists, false otherwise. */ protected boolean intersectRecords(int fromIndex, boolean fromSign, int toIndex, boolean toSign) { Automaton.State fromState = from.states[fromIndex]; Automaton.State toState = to.states[toIndex]; if(fromSign || toSign) { int[] fromChildren = fromState.children; int[] toChildren = toState.children; TypeSystem.RecordState fromFields = (TypeSystem.RecordState) fromState.data; TypeSystem.RecordState toFields = (TypeSystem.RecordState) toState.data; boolean fromOpen = fromFields.isOpen; boolean toOpen = toFields.isOpen; if (fromChildren.length < toChildren.length && !fromOpen) { return !fromSign || !toSign; } else if (fromChildren.length > toChildren.length && !toOpen) { return !fromSign || !toSign; } else if (!fromSign && !fromOpen && toOpen) { return true; // guaranteed true! } else if (!toSign && !toOpen && fromOpen) { return true; // guaranteed true! } boolean andChildren = true; boolean orChildren = false; int fi=0; int ti=0; while(fi != fromFields.size() && ti != toFields.size()) { boolean v; String fn = fromFields.get(fi); String tn = toFields.get(ti); int c = fn.compareTo(tn); if(c == 0) { int fromChild = fromChildren[fi++]; int toChild = toChildren[ti++]; v = isIntersection(fromChild, fromSign, toChild, toSign); } else if(c < 0 && toOpen) { fi++; v = toSign; } else if(c > 0 && fromOpen) { ti++; v = fromSign; } else { return !fromSign || !toSign; } andChildren &= v; orChildren |= v; } if(fi < fromFields.size()) { if(toOpen) { // assert fromSign || fromOpen orChildren |= toSign; andChildren &= toSign; } else { return !fromSign || !toSign; } } else if(ti < toFields.size()) { if(fromOpen) { // assert toSign || toOpen orChildren |= fromSign; andChildren &= fromSign; } else { return !fromSign || !toSign; } } if(!fromSign || !toSign) { return orChildren; } else { return andChildren; } } return true; } private int indexOf(int fromIndex, boolean fromSign, int toIndex, boolean toSign) { int to_size = to.size(); if(fromSign) { fromIndex += from.size(); } if(toSign) { toIndex += to_size; } return (fromIndex*to_size*2) + toIndex; } private static int invert(int kind, boolean sign) { if(sign) { return kind; } switch(kind) { case TypeSystem.K_ANY: return TypeSystem.K_VOID; case TypeSystem.K_VOID: return TypeSystem.K_ANY; case TypeSystem.K_UNION: return TypeSystem.K_INTERSECTION; default: return kind; } } }