All Downloads are FREE. Search and download functionalities are using the official Maven repository.

wyil.util.IncrementalSubtypingEnvironment Maven / Gradle / Ivy

// Copyright 2011 The Whiley Project Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wyil.util;

import static wyil.lang.WyilFile.TYPE_any;
import static wyil.lang.WyilFile.TYPE_array;
import static wyil.lang.WyilFile.TYPE_bool;
import static wyil.lang.WyilFile.TYPE_byte;
import static wyil.lang.WyilFile.TYPE_existential;
import static wyil.lang.WyilFile.TYPE_function;
import static wyil.lang.WyilFile.TYPE_int;
import static wyil.lang.WyilFile.TYPE_method;
import static wyil.lang.WyilFile.TYPE_nominal;
import static wyil.lang.WyilFile.TYPE_null;
import static wyil.lang.WyilFile.TYPE_property;
import static wyil.lang.WyilFile.TYPE_record;
import static wyil.lang.WyilFile.TYPE_reference;
import static wyil.lang.WyilFile.TYPE_tuple;
import static wyil.lang.WyilFile.TYPE_union;
import static wyil.lang.WyilFile.TYPE_universal;
import static wyil.lang.WyilFile.TYPE_unknown;
import static wyil.lang.WyilFile.TYPE_void;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

import wycc.util.ArrayUtils;
import wycc.util.AbstractCompilationUnit.Tuple;
import wyil.lang.WyilFile.Decl;
import wyil.lang.WyilFile.Expr;
import wyil.lang.WyilFile.QualifiedName;
import wyil.lang.WyilFile.Template;
import wyil.lang.WyilFile.Type;
import wyil.util.Subtyping.Constraints;
import wyil.util.Subtyping.LowerBoundConstraint;
import wyil.util.Subtyping.UpperBoundConstraint;

/**
 * 

* Provides default implementations for Subtyping.Environment. The * intention is that these be overriden to provide different variants (e.g. * relaxed subtype operators, etc). *

*

* (Subtyping) The default subtype operator checks whether one type is a * strict subtype of another. Unlike other subtype operators, this takes * into account the invariants on types. Consider these two types: * *

 * type nat is (int x) where x >= 0
 * type pos is (nat x) where x > 0
 * type tan is (int x) where x >= 0
 * 
* * In this case, we have nat <: int since int is * explicitly included in the definition of nat. Observe that this * applies transitively and, hence, pos <: nat. But, it does not * follow that nat <: int and, likewise, that * pos <: nat. Likewise, nat <: tan does not follow * (despite this being actually true) since we cannot reason about invariants. *

*

* (Binding) An important task is computing a "binding" between a * function, method or property declaration and a given set of concrete * arguments types. For example, consider: *

* *
 * template
 * function get(T[] items, int i) -> T:
 *    return items[i]
 *
 *  function f(int[] items) -> int:
 *     return get(items,0)
 * 
* *

* At the point of the invocation for get() we must resolve the * declared type function(T[],int)->(T) against the declared * parameter types (int[],int), yielding a binding * T=int. *

*

* Computing the binding between two types is non-trivial in Whiley. In addition * to template arguments (as above), we must handle lifetime arguments. For * example: *

* *
 * method  m(&a:int x) -> int:
 *    return *a
 *
 * ...
 *   &this:int ptr = new 1
 *   return m(ptr)
 * 
*

* At the invocation to m(), we need to infer the binding * a=this. A major challenge is the presence of union types. For * example, consider this binding problem: *

* *
 * template
 * function f(S x, S|T y) -> S|T:
 *    return y
 *
 * function g(int p, bool|int q) -> (bool|int r):
 *    return f(p,q)
 * 
*

* At the invocation to f we must generate the binding * S=int,T=bool. When binding bool|int against * S|T we need to consider both cases where * S=bool,T=int and S=int,T=bool. Otherwise, we cannot * be sure to consider the right combination. *

* * @author David J. Pearce * */ public class IncrementalSubtypingEnvironment implements Subtyping.Environment { /** * A constant representing the set of empty constraints. */ public final SubtypeConstraints TOP = new SubtypeConstraints(this); /** * The empty constraint set which is, by construction, invalid. */ public final SubtypeConstraints BOTTOM = new SubtypeConstraints(0, (ConcreteSolution) null, true, null, null); /** * A constant representing the set of empty constraint sets. */ public final IncrementalSubtypingEnvironment.AbstractConstraintsSet EMPTY_CONSTRAINT_SET = new AbstractConstraintsSet(TOP); /** * A constant representing an invalidated set of constraints */ public final IncrementalSubtypingEnvironment.AbstractConstraintsSet BOTTOM_CONSTRAINT_SET = new AbstractConstraintsSet(BOTTOM); protected final Map withins; public IncrementalSubtypingEnvironment() { this.withins = new HashMap<>(); } public IncrementalSubtypingEnvironment(Map withins) { this.withins = new HashMap<>(withins); } @Override public boolean isSatisfiableSubtype(Type t1, Type t2) { Subtyping.Constraints constraints = isSubtype(t1, t2); return constraints.isSatisfiable(); } @Override public Subtyping.Constraints isSubtype(Type t1, Type t2) { return isSubtype(t1, t2, null); } @Override public boolean isEmpty(QualifiedName nid, Type type) { return isContractive(nid, type, null); } // =========================================================================== // Contractivity // =========================================================================== @Override public boolean isWithin(String inner, String outer) { // if (outer.equals("*") || inner.equals(outer)) { // Cover easy cases first return true; } else { String[] outers = withins.get(inner); return outers != null && (ArrayUtils.firstIndexOf(outers, outer) >= 0); } } @Override public IncrementalSubtypingEnvironment declareWithin(String inner, String... outers) { IncrementalSubtypingEnvironment nenv = new IncrementalSubtypingEnvironment(withins); nenv.withins.put(inner, outers); return nenv; } @Override public String toString() { String r = "{"; boolean firstTime = true; for (Map.Entry w : withins.entrySet()) { if (!firstTime) { r += ", "; } firstTime = false; r = r + w.getKey() + " < " + Arrays.toString(w.getValue()); } return r + "}"; } // =========================================================================== // Contractivity // =========================================================================== /** * Provides a helper implementation for isContractive. * * @param name * @param type * @param visited * @return */ static boolean isContractive(QualifiedName name, Type type, HashSet visited) { switch (type.getOpcode()) { case TYPE_void: case TYPE_null: case TYPE_bool: case TYPE_int: case TYPE_property: case TYPE_byte: case TYPE_universal: case TYPE_unknown: case TYPE_function: case TYPE_method: return true; case TYPE_reference: { Type.Reference t = (Type.Reference) type; return isContractive(name, t.getElement(), visited); } case TYPE_array: { Type.Array t = (Type.Array) type; return isContractive(name, t.getElement(), visited); } case TYPE_record: { Type.Record t = (Type.Record) type; Tuple fields = t.getFields(); for (int i = 0; i != fields.size(); ++i) { if (isContractive(name, fields.get(i).getType(), visited)) { return true; } } return false; } case TYPE_union: { Type.Union c = (Type.Union) type; for (int i = 0; i != c.size(); ++i) { if (isContractive(name, c.get(i), visited)) { return true; } } return false; } default: case TYPE_nominal: Type.Nominal n = (Type.Nominal) type; Decl.Link link = n.getLink(); Decl.Type decl = link.getTarget(); QualifiedName nid = decl.getQualifiedName(); if (nid.equals(name)) { // We have identified a non-contractive type. return false; } else if (visited != null && visited.contains(nid)) { // NOTE: this identifies a type (other than the one we are looking for) which is // not contractive. It may seem odd then, that we pretend it is in fact // contractive. The reason for this is simply that we cannot tell here with the // type we are interested in is contractive or not. Thus, to improve the error // messages reported we ignore this non-contractiveness here (since we know // it'll be caught down the track anyway). return true; } else { // Lazily construct the visited set as, in the vast majority of cases, this is // never required. visited = (visited != null) ? visited : new HashSet<>(); // Add the name to prevent infinite loops visited.add(nid); } return isContractive(name, decl.getType(), visited); } } // =========================================================================== // Subtyping // =========================================================================== /** * A subtype operator aimed at checking whether one type is a strict * subtype of another. Unlike other subtype operators, this takes into * account the invariants on types. Consider these two types: * *
	 * type nat is (int x) where x >= 0
	 * type pos is (nat x) where x > 0
	 * type tan is (int x) where x >= 0
	 * 
* * In this case, we have nat <: int since int is * explicitly included in the definition of nat. Observe that this * applies transitively and, hence, pos <: nat. But, it does not * follow that nat <: int and, likewise, that * pos <: nat. Likewise, nat <: tan does not follow * (despite this being actually true) since we cannot reason about invariants. * * @author David J. Pearce * */ protected Subtyping.Constraints isSubtype(Type t1, Type t2, BinaryRelation cache) { final Subtyping.Constraints result; // FIXME: only need to check for coinductive case when both types are recursive. // If either is not recursive, then are guaranteed to eventually terminate. if (cache != null && cache.get(t1, t2)) { return TOP; } else if (cache == null) { // Lazily construct cache. cache = new BinaryRelation.HashSet<>(); } cache.set(t1, t2, true); // Normalise opcodes to align based on class int t1_opcode = normalise(t1.getOpcode()); int t2_opcode = normalise(t2.getOpcode()); // if (t1_opcode == t2_opcode) { switch (t1_opcode) { case TYPE_any: case TYPE_void: case TYPE_null: case TYPE_bool: case TYPE_byte: case TYPE_int: return TOP; case TYPE_array: result = isSubtype((Type.Array) t1, (Type.Array) t2, cache); break; case TYPE_tuple: result = isSubtype((Type.Tuple) t1, (Type.Tuple) t2, cache); break; case TYPE_record: result = isSubtype((Type.Record) t1, (Type.Record) t2, cache); break; case TYPE_nominal: result = isSubtype((Type.Nominal) t1, (Type.Nominal) t2, cache); break; case TYPE_union: result = isSubtype(t1, (Type.Union) t2, cache); break; case TYPE_reference: result = isSubtype((Type.Reference) t1, (Type.Reference) t2, cache); break; case TYPE_method: case TYPE_function: case TYPE_property: result = isSubtype((Type.Callable) t1, (Type.Callable) t2, cache); break; case TYPE_universal: result = isSubtype((Type.Universal) t1, (Type.Universal) t2, cache); break; case TYPE_existential: result = isSubtype(t1, (Type.Existential) t2, cache); break; default: throw new IllegalArgumentException("unexpected type encountered: " + t1); } } else if (t1_opcode == TYPE_any || t2_opcode == TYPE_void) { result = TOP; } else if (t1_opcode == TYPE_existential) { result = isSubtype((Type.Existential) t1, t2, cache); } else if (t2_opcode == TYPE_existential) { result = isSubtype(t1, (Type.Existential) t2, cache); } else if (t2_opcode == TYPE_nominal && ((Type.Nominal) t2).isAlias()) { // NOTE: this case is curious, but necessary. Explicit syntax for aliases would // help here. result = isSubtype(t1, (Type.Nominal) t2, cache); } else if (t2_opcode == TYPE_union) { result = isSubtype(t1, (Type.Union) t2, cache); } else if (t1_opcode == TYPE_union) { result = isSubtype((Type.Union) t1, t2, cache); } else if (t2_opcode == TYPE_nominal) { result = isSubtype(t1, (Type.Nominal) t2, cache); } else if (t1_opcode == TYPE_nominal) { result = isSubtype((Type.Nominal) t1, (Type.Atom) t2, cache); } else { // Nothing else works except void result = BOTTOM; } // Reset cache cache.set(t1, t2, false); // return result; } protected Subtyping.Constraints isSubtype(Type.Array t1, Type.Array t2, BinaryRelation cache) { return isSubtype(t1.getElement(), t2.getElement(), cache); } protected Subtyping.Constraints isSubtype(Type.Tuple t1, Type.Tuple t2, BinaryRelation cache) { Subtyping.Constraints constraints = TOP; // Check elements one-by-one for (int i = 0; i != t1.size(); ++i) { Subtyping.Constraints ith = isSubtype(t1.get(i), t2.get(i), cache); if (ith == null || constraints == null) { return BOTTOM; } else { constraints = constraints.intersect(ith); } } // Done return constraints; } protected Subtyping.Constraints isSubtype(Type.Record t1, Type.Record t2, BinaryRelation cache) { Tuple t1_fields = t1.getFields(); Tuple t2_fields = t2.getFields(); // Sanity check number of fields are reasonable. if (t1_fields.size() > t2_fields.size()) { return BOTTOM; } else if (t2.isOpen() && !t1.isOpen()) { return BOTTOM; } else if (!t1.isOpen() && t1_fields.size() != t2.getFields().size()) { return BOTTOM; } Subtyping.Constraints constraints = TOP; // NOTE: the following is O(n^2) but, in reality, will be faster than the // alternative (sorting fields into an array). That's because we expect a very // small number of fields in practice. for (int i = 0; i != t1_fields.size(); ++i) { Type.Field f1 = t1_fields.get(i); boolean matched = false; for (int j = 0; j != t2_fields.size(); ++j) { Type.Field f2 = t2_fields.get(j); if (f1.getName().equals(f2.getName())) { Subtyping.Constraints other = isSubtype(f1.getType(), f2.getType(), cache); // Matched field matched = true; constraints = constraints.intersect(other); } } // Check we actually matched the field! if (!matched) { return BOTTOM; } } // Done return constraints; } protected Subtyping.Constraints isSubtype(Type.Reference t1, Type.Reference t2, BinaryRelation cache) { Subtyping.Constraints first = isWidthSubtype(t1.getElement(), t2.getElement(), cache); // Sanity check what's going on if (first.isSatisfiable()) { return first; } else { return areEquivalent(t1.getElement(), t2.getElement(), cache); } } protected Subtyping.Constraints isWidthSubtype(Type t1, Type t2, BinaryRelation cache) { // NOTE: this method could be significantly improved by allowing recursive width // subtyping. if (t1 instanceof Type.Nominal) { Type.Nominal n1 = (Type.Nominal) t1; return isWidthSubtype(n1.getConcreteType(), t2, cache); } else if (t2 instanceof Type.Nominal) { Type.Nominal n2 = (Type.Nominal) t2; return isWidthSubtype(t1, n2.getConcreteType(), cache); } else if (t1 instanceof Type.Record && t2 instanceof Type.Record) { Subtyping.Constraints constraints = TOP; Type.Record r1 = (Type.Record) t1; Type.Record r2 = (Type.Record) t2; Tuple r1_fields = r1.getFields(); Tuple r2_fields = r2.getFields(); if (r1.isOpen() && r1_fields.size() <= r2_fields.size()) { for (int i = 0; i != r1_fields.size(); ++i) { Type.Field f1 = r1_fields.get(i); Type.Field f2 = r2_fields.get(i); if (!f1.getName().equals(f2.getName())) { // Fields have differing names return BOTTOM; } Subtyping.Constraints other = areEquivalent(f1.getType(), f2.getType(), cache); constraints = constraints.intersect(other); } return constraints; } } return BOTTOM; } protected Subtyping.Constraints isSubtype(Type.Callable t1, Type.Callable t2, BinaryRelation cache) { Type t1_params = t1.getParameter(); Type t2_params = t2.getParameter(); Type t1_return = t1.getReturn(); Type t2_return = t2.getReturn(); // Eliminate easy cases first if(!isCallableSubtype(t1.getOpcode(),t2.getOpcode())) { return BOTTOM; } // Check parameters (contra-variant) Subtyping.Constraints c_params = isSubtype(t2_params, t1_params, cache); // Check returns (co-variant) Subtyping.Constraints c_returns = isSubtype(t1_return, t2_return, cache); // Done return c_params.intersect(c_returns); } protected Subtyping.Constraints isSubtype(Type.Universal t1, Type.Universal t2, BinaryRelation cache) { if (t1.getOperand().equals(t2.getOperand())) { return TOP; } else { return BOTTOM; } } protected Subtyping.Constraints isSubtype(Type.Existential t1, Type t2, BinaryRelation cache) { if(t2 instanceof Type.Void) { return TOP; } else { return new SubtypeConstraints(this, new LowerBoundConstraint(this, t1, t2)); } } protected Subtyping.Constraints isSubtype(Type t1, Type.Existential t2, BinaryRelation cache) { if(t1 instanceof Type.Any) { return TOP; } else { return new SubtypeConstraints(this, new UpperBoundConstraint(this, t1, t2)); } } protected Subtyping.Constraints isSubtype(Type t1, Type.Union t2, BinaryRelation cache) { Subtyping.Constraints constraints = TOP; for (int i = 0; i != t2.size(); ++i) { Subtyping.Constraints other = isSubtype(t1, t2.get(i), cache); constraints = constraints.intersect(other); } return constraints; } protected Subtyping.Constraints isSubtype(Type.Union t1, Type t2, BinaryRelation cache) { Subtyping.Constraints winner = BOTTOM; // Select the winning bound. In principle, we actually could do better here but // this would mean allowing for disjunctions of constraints. for (int i = 0; i != t1.size(); ++i) { Subtyping.Constraints ith = isSubtype(t1.get(i), t2, cache); // Check whether we found a match or not if(ith != BOTTOM && winner != BOTTOM) { return BOTTOM; } else if(ith != BOTTOM) { winner = ith; } } // return winner; } protected Subtyping.Constraints isSubtype(Type.Nominal t1, Type.Nominal t2, BinaryRelation cache) { Decl.Type d1 = t1.getLink().getTarget(); Decl.Type d2 = t2.getLink().getTarget(); // Tuple t1_invariant = d1.getInvariant(); Tuple t2_invariant = d2.getInvariant(); // Dispatch easy cases if (d1 == d2) { // NOTE: at this point it is possible to only consider the template arguments // provided. However, doing this requires knowledge of the variance requirements // for each template parameter position. Such information is currently // unavailable though, in principle, could be inferred. Tuple template = d1.getTemplate(); Tuple t1_params = t1.getParameters(); Tuple t2_params = t2.getParameters(); Subtyping.Constraints constraints = TOP; for (int i = 0; i != template.size(); ++i) { Template.Variable ith = template.get(i); Template.Variance v = ith.getVariance(); Type t1_ith_param = t1_params.get(i); Type t2_ith_param = t2_params.get(i); // Apply constraints if (v == Template.Variance.COVARIANT || v == Template.Variance.INVARIANT) { constraints = constraints.intersect(isSubtype(t1_ith_param, t2_ith_param, cache)); } if (v == Template.Variance.CONTRAVARIANT || v == Template.Variance.INVARIANT) { constraints = constraints.intersect(isSubtype(t2_ith_param, t1_ith_param, cache)); } } return constraints; } else if(isAncestorOf(t1,t2)) { return isSubtype(t1.getConcreteType(), t2.getConcreteType()); } else { boolean left = isSubtype(t1_invariant, t2_invariant); boolean right = isSubtype(t2_invariant, t1_invariant); if (left || right) { Type tt1 = left ? t1.getConcreteType() : t1; Type tt2 = right ? t2.getConcreteType() : t2; return isSubtype(tt1, tt2, cache); } else { return BOTTOM; } } } /** * Check whether a nominal type is a subtype of an atom (i.e. not a nominal or * union). For example, int :> nat or {nat f} :> rec. * This is actually easy as an invariants on the nominal type can be ignored * (since they already imply it is a subtype). * * @param t1 * @param t2 * @param lifetimes * @return */ protected Subtyping.Constraints isSubtype(Type t1, Type.Nominal t2, BinaryRelation cache) { return isSubtype(t1, t2.getConcreteType(), cache); } /** * Check whether a nominal type is a supertype of an atom (i.e. not a nominal or * union). For example, int <: nat or {nat f} <: rec. * This is harder because the invariant cannot be reasoned about. In fact, the * only case where this can hold true is when there is no invariant. * * @param t1 * @param t2 * @param lifetimes * @return */ protected Subtyping.Constraints isSubtype(Type.Nominal t1, Type t2, BinaryRelation cache) { // Decl.Type d1 = t1.getLink().getTarget(); Tuple t1_invariant = d1.getInvariant(); // Dispatch easy cases if (isSubtype(t1_invariant, EMPTY_INVARIANT)) { return isSubtype(t1.getConcreteType(), t2, cache); } else { return BOTTOM; } } /** * Determine whether one invariant is a subtype of another. In other words, the * subtype invariant implies the supertype invariant. * * @param lhs The "super" type * @param rhs The "sub" type * @return */ protected boolean isSubtype(Tuple lhs, Tuple rhs) { // NOTE: in principle, we could potentially do more here. return lhs.size() == 0 || lhs.equals(rhs); } /** * Determine whether two types are "equivalent" or not. * * @param t1 * @param t2 * @param lifetimes * @return */ private Subtyping.Constraints areEquivalent(Type t1, Type t2, BinaryRelation cache) { // NOTE: this is a temporary solution. Subtyping.Constraints left = isSubtype(t1, t2, cache); Subtyping.Constraints right = isSubtype(t2, t1, cache); // return left.intersect(right); } private static boolean isCallableSubtype(int lhs, int rhs) { switch(lhs) { case TYPE_method: return true; case TYPE_function: return rhs == TYPE_function || rhs == TYPE_property; default: return rhs == TYPE_property; } } // =========================================================================== // AbstractConstraintsSet // =========================================================================== /** * Provides a straightforward implementation of Typing. * * @author David J. Pearce * */ public static class AbstractConstraintsSet implements Constraints.Set { /** * Square matrix of typing environments */ private final Subtyping.Constraints[] rows; public AbstractConstraintsSet(Subtyping.Constraints... rows) { this.rows = rows; } /** * Return flag indicating whether there are no valid typings remaining, or not. * This is equivalent to height() == 0. * * @return */ @Override public boolean empty() { return rows.length == 0; } /** * Return the number of typings that remain under consideration. If this is * zero, then there are no valid typings and the original expression cannot be * typed. On the other hand, if there is more than one valid typing (at the end) * then the original expression is ambiguous. * * @return */ @Override public int height() { return rows.length; } @Override public Subtyping.Constraints get(int ith) { return rows[ith]; } @Override public Constraints.Set fold(Comparator comparator) { if (rows.length <= 1) { return this; } else { Subtyping.Constraints[] nrows = Arrays.copyOf(rows, rows.length); for (int i = 0; i != nrows.length; ++i) { Subtyping.Constraints ith = nrows[i]; if (ith == null) { continue; } for (int j = i + 1; j < nrows.length; ++j) { Subtyping.Constraints jth = nrows[j]; if (jth == null) { continue; } int c = comparator.compare(ith, jth); if (c < 0) { nrows[j] = null; } else if (c > 0) { nrows[i] = null; } } } nrows = ArrayUtils.removeAll(nrows, null); return new AbstractConstraintsSet(nrows); } } /** * Apply a given function to all rows of the typing matrix producing a * potentially updated set of typing constraints. As part of this process, rows * may be invalidated if they fail to meet some criteria. * * @param fn The mapping function which is applied to each row. This returns * either an updated row, or null, if the row is * invalidated. * @return */ @Override public Constraints.Set map(Function fn) { Subtyping.Constraints[] nrows = rows; // for (int i = 0; i < rows.length; i = i + 1) { final Subtyping.Constraints before = nrows[i]; Subtyping.Constraints after = fn.apply(before); if (before == after) { // No change, so do nothing continue; } else if (nrows == rows) { // something changed, so clone nrows = Arrays.copyOf(rows, rows.length); } nrows[i] = after.isSatisfiable() ? after : null; } // Remove all invalid rows nrows = ArrayUtils.removeAll(nrows, null); // Create new typing return new AbstractConstraintsSet(nrows); } /** * Project each row of the typing matrix into zero or more rows, thus producing * a potentially updated constraint set. As part of this process, rows may be * added or removed based on various criteria. * * @param fn The mapping function which is applied to each row. This returns * zero or more updated rows. Note that it should not return * null. * @return */ @Override public Constraints.Set project(Function fn) { ArrayList nrows = new ArrayList<>(); // for (int i = 0; i < rows.length; i = i + 1) { final Subtyping.Constraints before = rows[i]; Subtyping.Constraints[] after = fn.apply(before); for (int j = 0; j != after.length; ++j) { Subtyping.Constraints jth = after[j]; if (jth.isSatisfiable()) { nrows.add(jth); } } } // Subtyping.Constraints[] arr = nrows.toArray(new Subtyping.Constraints[nrows.size()]); // Create new typing return new AbstractConstraintsSet(arr); } @Override public void foreach(Consumer fn) { for (int i = 0; i != rows.length; ++i) { fn.accept(rows[i]); } } @Override public String toString() { if (rows.length == 0) { return "_|_"; } else { String r = ""; for (Subtyping.Constraints row : rows) { r = r + row; } return r; } } } // =========================================================================== // Solution // =========================================================================== /** * A constant representing an invalid solution to a set of subtyping * constraints. For example, given a solution [?0 :> int] if we * then constrain bool :> ?0 we end up with an invalid solution. */ public final IncrementalSubtypingEnvironment.ConcreteSolution INVALID_SOLUTION = new ConcreteSolution(this,null,null); /** * A constant representing an empty solution to a set of subtyping constraints. */ public final IncrementalSubtypingEnvironment.ConcreteSolution EMPTY_SOLUTION = new ConcreteSolution(this); /** * Represents a current best solution for a given typing problem. Specifically, * for a given set of n type variables, each variable has given * concrete upper and lower bounds. For example, we might have * [?0 :> int, int :> ?01]. * * Observe that every type within the lower and upper bounds are concrete (i.e. * do not contain existentials). * * @author David J. Pearce * */ public static class ConcreteSolution implements Constraints.Solution { /** * The environment is needed in order to determine when this solution becomes * invalid, and also to combine upper and lower bounds using the * leastUpperBound and greatestLowerBound operators. */ private final IncrementalSubtypingEnvironment environment; /** * The current set of upper bounds for given all known type variables. */ private final Type[] upperBounds; /** * The current set of lower bounds for given all known type variables. */ private final Type[] lowerBounds; public ConcreteSolution(IncrementalSubtypingEnvironment environment) { this.environment = environment; this.upperBounds = new Type[0]; this.lowerBounds = new Type[0]; } private ConcreteSolution(IncrementalSubtypingEnvironment environment, Type[] upperBounds, Type[] lowerBounds) { this.environment = environment; this.upperBounds = upperBounds; this.lowerBounds = lowerBounds; } @Override public Type get(int i) { Type f = floor(i); if(f instanceof Type.Void) { return ceil(i); } else { return f; } } @Override public Type floor(int i) { if (lowerBounds == null || i >= lowerBounds.length) { return Type.Void; } else { return lowerBounds[i]; } } @Override public Type ceil(int i) { if (upperBounds == null || i >= upperBounds.length) { return Type.Any; } else { return upperBounds[i]; } } @Override public int size() { return lowerBounds == null ? 0 : lowerBounds.length; } /** * Check whether a given solution is fully satisfied or not. In short, whether * or not any variables remain which have neither an upper or lower bound. Such * variables are problematic because we cannot determine a valid type for them * (i.e. as neither any nor void are valid types). If * the solution is satisfiable but not yet satisfied, then it means we need to * continue atttempt to find a solution. Note, however, than an unsatisfiable * solution is considered to be satisfied for simplicity. * * @param n * @return */ @Override public boolean isComplete(int n) { if(lowerBounds == null) { return true; } else { for (int i = 0; i < n; ++i) { Type upper = ceil(i); Type lower = floor(i); // NOTE: potential performance improvement here to avoid unnecessary subtype // checks by only testing bounds which have actually changed. if (upper instanceof Type.Any || lower instanceof Type.Void) { return false; } } return true; } } /** * Check whether the given solution is satisfiable or not. That is, whether or * not there are any bounds which definitely cannot be satisfied. For example, * {?0 :> int} is satisfiable with ?0=int. However, * {bool :> ?0 :> int} is not satisfiable. * * @param n * @param env * @return */ @Override public boolean isUnsatisfiable() { return lowerBounds == null; } /** * Constrain the solution of a given variable with a given (concrete) lower * bound. If the solution becomes invalid, return BOTTOM. * * @param i Variable to be constrained * @param nLowerBound New lowerbound to constrain with * @return */ @Override public IncrementalSubtypingEnvironment.ConcreteSolution constrain(int i, Type nLowerBound) { if(!Subtyping.isConcrete(nLowerBound)) { throw new IllegalArgumentException("Upper bound should be concrete"); } else if(lowerBounds == null) { // Intersecting an invalid solution always returns an invalid solution return this; } Type[] nUpperBounds = upperBounds; Type[] nLowerBounds = lowerBounds; Type lub; // Sanity check enough space if(i >= nLowerBounds.length) { nUpperBounds = expand(nUpperBounds, i+1, Type.Any); nLowerBounds = expand(nLowerBounds, i+1, Type.Void); // Lowerbound easy as nothing to do lub = nLowerBound; } else { // Compute lower bound Type lowerBound = nLowerBounds[i]; lub = environment.leastUpperBound(lowerBound,nLowerBound); // Check whether anything actually changed if(lub == lowerBound) { // No change in lower bound. Therefore, if solution valid before, it is still // valid now. return this; } // Clone lower bounds as will definitely update them nLowerBounds = Arrays.copyOf(nLowerBounds, nLowerBounds.length); } // Sanity check updated solution is still valid if (!environment.isSatisfiableSubtype(nUpperBounds[i], lub) || lub instanceof Type.Any) { return environment.INVALID_SOLUTION; } else { nLowerBounds[i] = lub; return new ConcreteSolution(environment, nUpperBounds, nLowerBounds); } } /** * Constraint the solution of a given variable with a given (concrete) upper * bound. If the solution becomes invalid, return BOTTOM. * * @param nUpperBound New upperbound to constrain with * @param i Variable to be constrained * @return */ @Override public IncrementalSubtypingEnvironment.ConcreteSolution constrain(Type nUpperBound, int i) { if(!Subtyping.isConcrete(nUpperBound)) { throw new IllegalArgumentException("Upper bound should be concrete"); } else if(lowerBounds == null) { // Intersecting an invalid solution always returns an invalid solution return this; } Type[] nUpperBounds = upperBounds; Type[] nLowerBounds = lowerBounds; Type glb; // Sanity check enough space if(i >= nLowerBounds.length) { nUpperBounds = expand(nUpperBounds, i+1, Type.Any); nLowerBounds = expand(nLowerBounds, i+1, Type.Void); // Lowerbound easy as nothing to do glb = nUpperBound; } else { // Compute lower bound Type upperBound = nUpperBounds[i]; glb = environment.greatestLowerBound(upperBound,nUpperBound); // Check whether anything actually changed if(glb == upperBound) { // No change in lower bound. Therefore, if solution valid before, it is still // valid now. return this; } // Clone lower bounds as will definitely update them nUpperBounds = Arrays.copyOf(nUpperBounds, nUpperBounds.length); } // Sanity check updated solution is still valid if(!environment.isSatisfiableSubtype(glb,nLowerBounds[i]) || glb instanceof Type.Void) { return environment.INVALID_SOLUTION; } else { nUpperBounds[i] = glb; return new ConcreteSolution(environment, nUpperBounds, nLowerBounds); } } @Override public boolean equals(Object o) { if (o instanceof IncrementalSubtypingEnvironment.ConcreteSolution) { IncrementalSubtypingEnvironment.ConcreteSolution s = (IncrementalSubtypingEnvironment.ConcreteSolution) o; return Arrays.equals(lowerBounds, s.lowerBounds) && Arrays.equals(upperBounds, s.upperBounds); } else { return false; } } @Override public int hashCode() { return Arrays.hashCode(lowerBounds) ^ Arrays.hashCode(upperBounds); } @Override public String toString() { if(lowerBounds == null) { return "⊥"; } else { String r = "["; int n = Math.max(lowerBounds.length, upperBounds.length); for (int i = 0; i != n; ++i) { if (i != 0) { r += ";"; } if(i < upperBounds.length && i < lowerBounds.length && upperBounds[i].equals(lowerBounds[i])) { r += "?" + i + "=" + upperBounds[i]; } else { if (i < upperBounds.length) { Type upper = upperBounds[i]; if (!(upper instanceof Type.Any)) { r += upper + " :> "; } } r += "?" + i; if (i < lowerBounds.length) { Type lower = lowerBounds[i]; if (!(lower instanceof Type.Void)) { r += " :> " + lower; } } } } return r + "]"; } } } // =========================================================================== // Greatest Lower Bound // =========================================================================== @Override public Type greatestLowerBound(Type t1, Type t2) { int t1_opcode = normalise(t1.getOpcode()); int t2_opcode = normalise(t2.getOpcode()); // if(t1_opcode == t2_opcode) { switch(t1_opcode) { case TYPE_void: case TYPE_any: case TYPE_null: case TYPE_bool: case TYPE_byte: case TYPE_int: return t1; case TYPE_array: return greatestLowerBound((Type.Array) t1,(Type.Array) t2); case TYPE_tuple: return greatestLowerBound((Type.Tuple) t1,(Type.Tuple) t2); case TYPE_record: return greatestLowerBound((Type.Record) t1,(Type.Record) t2); case TYPE_reference: return greatestLowerBound((Type.Reference) t1,(Type.Reference) t2); case TYPE_universal: return greatestLowerBound((Type.Universal) t1,(Type.Universal) t2); case TYPE_nominal: return greatestLowerBound((Type.Nominal) t1,(Type.Nominal) t2); case TYPE_method: case TYPE_function: case TYPE_property: return greatestLowerBound((Type.Callable) t1,(Type.Callable) t2); case TYPE_union: return greatestLowerBound((Type.Union) t1, (Type.Union) t2); } } // Handle special forms for t1 if(t1_opcode == TYPE_void || t2_opcode == TYPE_any) { return t1; } else if(t1_opcode == TYPE_any || t2_opcode == TYPE_void) { return t2; } else if(t1_opcode == TYPE_union) { return greatestLowerBound((Type.Union) t1, t2); } else if(t2_opcode == TYPE_union) { return greatestLowerBound(t1, (Type.Union) t2); } else if(t1_opcode == TYPE_nominal) { return greatestLowerBound((Type.Nominal) t1, t2); } else if(t2_opcode == TYPE_nominal) { return greatestLowerBound(t1, (Type.Nominal) t2); } // Default case, nothing else fits. return Type.Void; } public Type greatestLowerBound(Type.Array t1, Type.Array t2) { Type e1 = t1.getElement(); Type e2 = t2.getElement(); Type glb = greatestLowerBound(e1,e2); // if (e1 == glb) { return t1; } else if (e2 == glb) { return t2; } else if (glb instanceof Type.Void) { return Type.Void; } else { return new Type.Array(glb); } } public Type greatestLowerBound(Type.Tuple t1, Type.Tuple t2) { if (t1.size() == t2.size()) { boolean left = true; boolean right = true; Type[] types = new Type[t1.size()]; for (int i = 0; i != t1.size(); ++i) { Type l = t1.get(i); Type r = t2.get(i); Type glb = types[i] = greatestLowerBound(l, r); left &= (l == glb); right &= (r == glb); } // if (left) { return t1; } else if (right) { return t2; } else { return Type.Tuple.create(types); } } return Type.Void; } public Type greatestLowerBound(Type.Record t1, Type.Record t2) { final Tuple t1_fields = t1.getFields(); final Tuple t2_fields = t2.getFields(); // final int n = t1_fields.size(); final int m = t2_fields.size(); // Santise input for simplicity if(n < m) { return greatestLowerBound(t2,t1); } else if(!subset(t2,t2)) { return Type.Void; } else if (!t2.isOpen() && n != m) { return Type.Void; } // Check matching fields boolean left = true; boolean right = true; Type[] types = new Type[n]; for(int i=0;i!=types.length;++i) { Type.Field f = t1_fields.get(i); Type t1f = f.getType(); Type t2f = t2.getField(f.getName()); // NOTE: for open records t2f can be null Type glb = (t2f == null) ? t1f : greatestLowerBound(t1f, t2f); // if(glb instanceof Type.Void) { return Type.Void; } else { types[i] = glb; left &= (glb == t1f); right &= (glb == t2f); } } // if(left) { return t1; } else if(right && m == n) { return t2; } // Create record Type.Field[] nFields = new Type.Field[n]; for(int i=0;i!=nFields.length;++i) { Type.Field f = t1_fields.get(i); nFields[i] = new Type.Field(f.getName(),types[i]); } return new Type.Record(t1.isOpen() & t2.isOpen(),new Tuple<>(nFields)); } public Type greatestLowerBound(Type.Reference t1, Type.Reference t2) { Type e1 = t1.getElement(); Type e2 = t2.getElement(); if(isSatisfiableSubtype(e1,e2) && isSatisfiableSubtype(e2,e1)) { return t1; } else { return Type.Void; } } public Type greatestLowerBound(Type.Universal t1, Type.Universal t2) { if (t1.getOperand().get().equals(t2.getOperand().get())) { return t1; } else { return Type.Void; } } public Type greatestLowerBound(Type.Callable t1, Type.Callable t2) { int t1_opcode = t1.getOpcode(); Type t1_param = t1.getParameter(); Type t1_return = t1.getReturn(); int t2_opcode = t2.getOpcode(); Type t2_param = t2.getParameter(); Type t2_return = t2.getReturn(); // Type param = leastUpperBound(t1_param,t2_param); Type ret = greatestLowerBound(t1_return,t2_return); int opcode = greatestLowerBound(t1_opcode, t2_opcode); // if(t1_param == param && t1_return == ret && t1_opcode == opcode) { return t1; } else if(t2_param == param && t2_return == ret && t2_opcode == opcode) { return t2; } else if(param instanceof Type.Void || ret instanceof Type.Void) { return Type.Void; } else if(opcode == TYPE_property){ return new Type.Property(param, ret); } else if(opcode == TYPE_function) { return new Type.Function(param, ret); } else if(t1 instanceof Type.Method && t2 instanceof Type.Method) { throw new IllegalArgumentException("IMPLEMENT ME"); } else { return Type.Void; } } private static int greatestLowerBound(int lhs, int rhs) { if(lhs == TYPE_method || rhs == TYPE_method) { return TYPE_method; } else if(lhs == TYPE_function || rhs == TYPE_function) { return TYPE_function; } else { return TYPE_property; } } public Type greatestLowerBound(Type.Union t1, Type.Union t2) { // NOTE: this could be made more efficient. For example, by sorting bounds. Type t = greatestLowerBound(t1, (Type) t2); // FIXME: what an ugly solution :( if (t.equals(t1)) { return t1; } else if (t.equals(t2)) { return t2; } else { return t; } } public Type greatestLowerBound(Type.Nominal t1, Type.Nominal t2) { Decl.Type d1 = t1.getLink().getTarget(); Decl.Type d2 = t2.getLink().getTarget(); // Determine whether alias or not. final boolean d1_alias = t1.isAlias(); final boolean d2_alias = t2.isAlias(); // if (isSatisfiableSubtype(t2, t1)) { return t1; } else if (isSatisfiableSubtype(t1, t2)) { return t2; } else if (d1_alias) { return greatestLowerBound(t1.getConcreteType(), t2); } else if (d2_alias) { return greatestLowerBound(t1, t2.getConcreteType()); } else { return Type.Void; } } public Type greatestLowerBound(Type.Union t1, Type t2) { Type[] bounds = new Type[t1.size()]; boolean changed = false; for (int i = 0; i != bounds.length; ++i) { Type before = t1.get(i); Type after = greatestLowerBound(before, t2); bounds[i] = after; changed |= (before != after); } if(changed) { return Type.Union.create(bounds); } else { return t1; } } public Type greatestLowerBound(Type t1, Type.Union t2) { Type[] bounds = new Type[t2.size()]; boolean changed = false; for (int i = 0; i != bounds.length; ++i) { Type before = t2.get(i); Type after = greatestLowerBound(t1, before); bounds[i] = after; changed |= (before != after); } if (changed) { return Type.Union.create(bounds); } else { return t2; } } public Type greatestLowerBound(Type.Nominal t1, Type t2) { // REQUIRES: !(t2 instanceof Type.Nominal) && !(t2 instanceof Type.Union) Decl.Type d1 = t1.getLink().getTarget(); // Determine whether alias or not. final boolean alias = t1.isAlias(); // if(isSatisfiableSubtype(t2, t1)) { return t1; } else if (alias) { return greatestLowerBound(t1.getConcreteType(), t2); } else { return Type.Void; } } public Type greatestLowerBound(Type t1, Type.Nominal t2) { // REQUIRES: !(t1 instanceof Type.Nominal) && !(t1 instanceof Type.Union) Decl.Type d2 = t2.getLink().getTarget(); // Determine whether alias or not. final boolean alias = t2.isAlias(); // if (isSatisfiableSubtype(t1, t2)) { return t2; } else if (alias) { return greatestLowerBound(t1, t2.getConcreteType()); } else { return Type.Void; } } // =========================================================================== // Least Upper Bound // =========================================================================== @Override public Type leastUpperBound(Type t1, Type t2) { int t1_opcode = normalise(t1.getOpcode()); int t2_opcode = normalise(t2.getOpcode()); // if(t1_opcode == t2_opcode) { switch(t1_opcode) { case TYPE_void: case TYPE_any: case TYPE_null: case TYPE_bool: case TYPE_byte: case TYPE_int: return t1; case TYPE_array: return leastUpperBound((Type.Array) t1,(Type.Array) t2); case TYPE_tuple: return leastUpperBound((Type.Tuple) t1,(Type.Tuple) t2); case TYPE_record: return leastUpperBound((Type.Record) t1,(Type.Record) t2); case TYPE_reference: return leastUpperBound((Type.Reference) t1,(Type.Reference) t2); case TYPE_universal: return leastUpperBound((Type.Universal) t1,(Type.Universal) t2); case TYPE_nominal: return leastUpperBound((Type.Nominal) t1,(Type.Nominal) t2); case TYPE_method: case TYPE_function: case TYPE_property: return leastUpperBound((Type.Callable) t1,(Type.Callable) t2); case TYPE_union: return leastUpperBound((Type.Union) t1,(Type.Union) t2); } } if (t1_opcode == TYPE_void || t2_opcode == TYPE_any) { return t2; } else if (t1_opcode == TYPE_any || t2_opcode == TYPE_void) { return t1; } else if(t1_opcode == TYPE_union && t2_opcode == TYPE_union) { return leastUpperBound((Type.Union) t1, (Type.Union) t2); } else if(t1_opcode == TYPE_union) { return leastUpperBound((Type.Union) t1, t2); } else if(t2_opcode == TYPE_union) { return leastUpperBound(t1, (Type.Union) t2); } else if(t1_opcode == TYPE_nominal && t2_opcode == TYPE_nominal) { return leastUpperBound((Type.Nominal) t1, (Type.Nominal) t2); } else if(t1_opcode == TYPE_nominal) { return leastUpperBound((Type.Nominal) t1, t2); } else if(t2_opcode == TYPE_nominal) { return leastUpperBound(t1, (Type.Nominal) t2); } // Default case, nothing else fits. return new Type.Union(t1,t2); } public Type leastUpperBound(Type.Array t1, Type.Array t2) { Type e1 = t1.getElement(); Type e2 = t2.getElement(); Type lub = leastUpperBound(e1,e2); // if (e1 == lub) { return t1; } else if (e2 == lub) { return t2; } else { return new Type.Array(lub); } } public Type leastUpperBound(Type.Tuple t1, Type.Tuple t2) { if (t1.size() != t2.size()) { return new Type.Union(t1, t2); } else { boolean left = true; boolean right = true; Type[] types = new Type[t1.size()]; for (int i = 0; i != t1.size(); ++i) { Type l = t1.get(i); Type r = t2.get(i); Type lub = types[i] = leastUpperBound(l, r); left &= (l == lub); right &= (r == lub); } // if (left) { return t1; } else if (right) { return t2; } else { return Type.Tuple.create(types); } } } public Type leastUpperBound(Type.Record t1, Type.Record t2) { Tuple t1_fields = t1.getFields(); Tuple t2_fields = t2.getFields(); // Order fields for simplicity if(t1_fields.size() > t2_fields.size()) { return leastUpperBound(t2,t1); } else if(!subset(t2,t1)) { // Some fields in t1 not in t2 return new Type.Union(t1,t2); } else if(!subset(t1,t2)) { // Some fields in t1 not in t2 return new Type.Union(t1,t2); } // ASSERT: |t1_fields| == |t2_fields| Type.Field[] fields = new Type.Field[t1_fields.size()]; boolean left = true; boolean right = true; for(int i=0;i!=t1_fields.size();++i) { Type.Field f = t1_fields.get(i); Type t1f = f.getType(); Type t2f = t2.getField(f.getName()); Type lub = leastUpperBound(t1f,t2f); fields[i] = new Type.Field(f.getName(),lub); left &= (t1f == lub); right &= (t2f == lub); } if(left) { return t1; } else if(right) { return t2; } else { boolean isOpen = t1.isOpen() | t2.isOpen(); return new Type.Record(isOpen,new Tuple<>(fields)); } } /** * Check that lhs fields are a subset of rhs fields * * @param lhs * @param rhs * @return */ public boolean subset(Type.Record lhs, Type.Record rhs) { Tuple lhs_fields = lhs.getFields(); for(int i=0;i!=lhs_fields.size();++i) { Type.Field ith = lhs_fields.get(i); if(rhs.getField(ith.getName()) == null) { return false; } } return true; } public Type leastUpperBound(Type.Reference t1, Type.Reference t2) { Type e1 = t1.getElement(); Type e2 = t2.getElement(); if(isSatisfiableSubtype(e1,e2) && isSatisfiableSubtype(e2,e1)) { return t1; } else { return new Type.Union(t1,t2); } } public Type leastUpperBound(Type.Universal t1, Type.Universal t2) { if (t1.getOperand().get().equals(t2.getOperand().get())) { return t1; } else { return new Type.Union(t1, t2); } } public Type leastUpperBound(Type.Callable t1, Type.Callable t2) { if(t1.getOpcode() != t2.getOpcode()) { return new Type.Union(t1,t2); } else { Type t1_param = t1.getParameter(); Type t1_return = t1.getReturn(); Type t2_param = t2.getParameter(); Type t2_return = t2.getReturn(); Type glb_param = leastUpperBound(t1_param,t2_param); Type glb_return = leastUpperBound(t1_return,t2_return); // if(glb_param == t1_param && glb_return == t1_return) { return t1; } else if(glb_param == t2_param && glb_return == t2_return) { return t2; } else if(t1 instanceof Type.Function) { return new Type.Function(glb_param, glb_return); } else if(t1 instanceof Type.Property) { return new Type.Property(glb_param, glb_return); } else { Type.Method m1 = (Type.Method) t1; Type.Method m2 = (Type.Method) t2; // FIXME: this is broken return new Type.Method(glb_param, glb_return); } } } public Type leastUpperBound(Type.Union t1, Type.Union t2) { Type[] types = new Type[t1.size() * t2.size()]; int k = 0; for(int i=0;i!=t1.size();++i) { Type ith = t1.get(i); for(int j=0;j!=t2.size();++j) { types[k++] = leastUpperBound(ith,t2.get(j)); } } // NOTE: this is needed to adhere to the contract for lub Type r = Type.Union.create(types); if (r.equals(t1)) { return t1; } else if (r.equals(t2)) { return t2; } else { // return r; } } public Type leastUpperBound(Type.Nominal t1, Type.Nominal t2) { Decl.Type d1 = t1.getLink().getTarget(); Decl.Type d2 = t2.getLink().getTarget(); // Determine whether alias or not. final boolean d1_alias = (d1.getInvariant().size() == 0); final boolean d2_alias = (d2.getInvariant().size() == 0); // if (isSatisfiableSubtype(t1, t2)) { return t1; } else if (isSatisfiableSubtype(t2, t1)) { return t2; } else if (d1_alias) { return leastUpperBound(t1.getConcreteType(), t2); } else if (d2_alias) { return leastUpperBound(t1, t2.getConcreteType()); } else { return new Type.Union(t1, t2); } } public Type leastUpperBound(Type.Nominal t1, Type t2) { Decl.Type d1 = t1.getLink().getTarget(); // Determine whether alias or not. final boolean d1_alias = (d1.getInvariant().size() == 0); // if (isSatisfiableSubtype(t2, t1)) { return t2; } else if (d1_alias) { return leastUpperBound(t1.getConcreteType(), t2); } else { return new Type.Union(t1, t2); } } public Type leastUpperBound(Type.Union t1, Type t2) { // FIXME: this is freakin' broken Type[] types = ArrayUtils.removeDuplicates(ArrayUtils.append(t1.getAll(), t2)); if(types.length == t1.size()) { return t1; } else { return new Type.Union(types); } } public Type leastUpperBound(Type t1, Type.Nominal t2) { Decl.Type d2 = t2.getLink().getTarget(); // Determine whether alias or not. final boolean d2_alias = (d2.getInvariant().size() == 0); // if (isSatisfiableSubtype(t1, t2)) { return t2; } else if (d2_alias) { return leastUpperBound(t1,t2.getConcreteType()); } else { return new Type.Union(t1, t2); } } public Type leastUpperBound(Type t1, Type.Union t2) { // FIXME: this is freakin' broken Type[] types = ArrayUtils.removeDuplicates(ArrayUtils.append(t1, t2.getAll())); if (types.length == t2.size()) { return t2; } else { return new Type.Union(types); } } // =============================================================================== // Type Subtraction // =============================================================================== /** * Subtract one type from another. * * @param variable * @param lowerBound * @return */ @Override public Type subtract(Type t1, Type t2) { return subtract(t1, t2, new BinaryRelation.HashSet<>()); } private Type subtract(Type t1, Type t2, BinaryRelation cache) { // FIXME: only need to check for coinductive case when both types are recursive. // If either is not recursive, then are guaranteed to eventually terminate. if (cache != null && cache.get(t1, t2)) { return Type.Void; } else if (cache == null) { // Lazily construct cache. cache = new BinaryRelation.HashSet<>(); } cache.set(t1, t2, true); // int t1_opcode = t1.getOpcode(); int t2_opcode = t2.getOpcode(); // if (t1.equals(t2)) { // Easy case return Type.Void; } else if (t1_opcode == t2_opcode) { switch (t1_opcode) { case TYPE_void: case TYPE_null: case TYPE_bool: case TYPE_byte: case TYPE_int: return Type.Void; case TYPE_array: case TYPE_reference: case TYPE_method: case TYPE_function: case TYPE_property: return t1; case TYPE_record: return subtract((Type.Record) t1, (Type.Record) t2, cache); case TYPE_nominal: return subtract((Type.Nominal) t1, (Type.Nominal) t2, cache); case TYPE_union: return subtract((Type.Union) t1, t2, cache); default: throw new IllegalArgumentException("unexpected type encountered: " + t1); } } else if (t2_opcode == TYPE_union) { return subtract(t1, (Type.Union) t2, cache); } else if (t1_opcode == TYPE_union) { return subtract((Type.Union) t1, t2, cache); } else if (t2_opcode == TYPE_nominal) { return subtract(t1, (Type.Nominal) t2, cache); } else if (t1_opcode == TYPE_nominal) { return subtract((Type.Nominal) t1, t2, cache); } else { return t1; } } /** * Subtraction of records is possible in a limited number of cases. * * @param t1 * @param t2 * @return */ public Type subtract(Type.Record t1, Type.Record t2, BinaryRelation cache) { Tuple t1_fields = t1.getFields(); Tuple t2_fields = t2.getFields(); if (t1_fields.size() != t2_fields.size() || t1.isOpen() || t1.isOpen()) { // Don't attempt anything return t1; } Type.Field[] r_fields = new Type.Field[t1_fields.size()]; boolean found = false; for (int i = 0; i != t1_fields.size(); ++i) { Type.Field f1 = t1_fields.get(i); Type.Field f2 = t2_fields.get(i); if (!f1.getName().equals(f2.getName())) { // Give up return t1; } if (!f1.getType().equals(f2.getType())) { if (found) { return t1; } else { found = true; Type tmp = subtract(f1.getType(), f2.getType(), cache); r_fields[i] = new Type.Field(f1.getName(), tmp); } } else { r_fields[i] = f1; } } return new Type.Record(false, new Tuple<>(r_fields)); } public Type subtract(Type.Nominal t1, Type.Nominal t2, BinaryRelation cache) { // Decl.Type d1 = t1.getLink().getTarget(); // NOTE: the following invariant check is essentially something akin to // determining whether or not this is a union. if (d1.getInvariant().size() == 0) { return subtract(t1.getConcreteType(), (Type) t2, cache); } else { return t1; } } public Type subtract(Type t1, Type.Nominal t2, BinaryRelation cache) { Decl.Type d2 = t2.getLink().getTarget(); // NOTE: the following invariant check is essentially something akin to // determining whether or not this is a union. if (d2.getInvariant().size() == 0) { return subtract(t1, t2.getConcreteType(), cache); } else { return t1; } } public Type subtract(Type.Nominal t1, Type t2, BinaryRelation cache) { Decl.Type d1 = t1.getLink().getTarget(); // NOTE: the following invariant check is essentially something akin to // determining whether or not this is a union. if (d1.getInvariant().size() == 0) { return subtract(t1.getConcreteType(), t2, cache); } else { return t1; } } public Type subtract(Type t1, Type.Union t2, BinaryRelation cache) { for (int i = 0; i != t2.size(); ++i) { t1 = subtract(t1, t2.get(i), cache); } return t1; } public Type subtract(Type.Union t1, Type t2, BinaryRelation cache) { Type[] types = new Type[t1.size()]; for (int i = 0; i != t1.size(); ++i) { types[i] = subtract(t1.get(i), t2, cache); } // Remove any selected cases return Type.Union.create(ArrayUtils.removeAll(types, Type.Void)); } // ================================================================================ // Constrains // ================================================================================ /** * Represents a set of constraints of the form ? :> T or * T :> ? and a valid solution. symbolic constraints are * those where T itself contains existential variables. In * contrast, concrete constraints are those where T itself * is concrete. In the current implementation, symbolic constraints are kept as * is whilst concrete constraints are immediately applied to the active * solution. * * @author David J. Pearce * */ private class SubtypeConstraints implements Subtyping.Constraints { /** * A parent pointer to the enclosing environment. This is necessary for access * to the subtype operator. */ private final IncrementalSubtypingEnvironment environment; /** * The set of subtyping constraints that this class embodies. The key is that we * want to able to solve these constraints easily to determine whether or not * they are satisfiable. */ private Subtyping.Constraint[] constraints; /** * A cache of the maximum number of variables used in any of the constraints. * This just helps us know when we have a complete solution or not. */ private final int nVariables; /** * The best current (valid) solution to the given set of constraints. This can * be null if none computed yet. It can also be out-of-date with * respect to the given set of constraints. */ private Subtyping.Constraints.Solution candidate; /** * Dirty flag indicates whether or not the candidate solution is up to date with * the given constraints. This allows us to be lazy in closing over constraints * whilst they are being accumulated through intersection operations prior to an * satisfiability query. */ private boolean dirty; public SubtypeConstraints(IncrementalSubtypingEnvironment environment, Subtyping.Constraint... constraints) { this.environment = environment; this.constraints = ArrayUtils.removeDuplicates(constraints); this.candidate = null; this.nVariables = Subtyping.numberOfVariables(constraints); } private SubtypeConstraints(int n, Subtyping.Constraints.Solution candidate, boolean dirty, Subtyping.Constraint[] constraints, IncrementalSubtypingEnvironment environment) { this.environment = environment; this.constraints = constraints; this.candidate = candidate; this.dirty = dirty; this.nVariables = n; } @Override public boolean isSatisfiable() { // NOTE: this is a very simple and largely broken formulation. if (constraints == null) { return false; } else if (dirty || candidate == null) { // NOTE: this is a hack this.candidate = close(environment.EMPTY_SOLUTION, constraints, environment); this.dirty = false; } return !candidate.isUnsatisfiable(); } @Override public int size() { if (constraints == null) { return 0; } else { return constraints.length; } } @Override public int maxVariable() { return nVariables - 1; } @Override public SubtypeConstraints fresh(int n) { return new SubtypeConstraints(nVariables + n, candidate, dirty, constraints, environment); } @Override public Subtyping.Constraint get(int ith) { return constraints[ith]; } @Override public SubtypeConstraints intersect(Subtyping.Constraint... oconstraints) { if (constraints == null) { return BOTTOM; } else { // NOTE: performance optimisation possible here Subtyping.Constraint[] nconstraints = ArrayUtils.append(constraints, oconstraints); // Remove all duplicates nconstraints = ArrayUtils.removeDuplicates(nconstraints); // Check what happened if (nconstraints.length == constraints.length) { // Nothing changed! return this; } else { int max = Math.max(nVariables, Subtyping.numberOfVariables(oconstraints)); return new SubtypeConstraints(max, candidate, true, nconstraints, environment); } } } @Override public SubtypeConstraints intersect(Subtyping.Constraints other) { // FIXME: we could do better here. return intersect((SubtypeConstraints) other); } /** * Intersect this constraint set with another. This essentially determines the * cross-product of rows in the two constraint sets. * * @param other * @return */ public SubtypeConstraints intersect(SubtypeConstraints other) { if (constraints == null || other.constraints == null) { return BOTTOM; } else { // NOTE: performance optimisation possible here Subtyping.Constraint[] nconstraints = ArrayUtils.append(constraints, other.constraints); // Remove all duplicates nconstraints = ArrayUtils.removeDuplicates(nconstraints); // Check what happened if (nconstraints.length == constraints.length) { // Nothing changed! return this; } else { // FIXME: could attempt to update solution here which might provide some // performance gains. return new SubtypeConstraints(Math.max(nVariables, other.nVariables), candidate, true, nconstraints, environment); } } } @Override public Subtyping.Constraints.Solution solve(int n) { return (candidate != null) ? candidate : environment.EMPTY_SOLUTION; } @Override public int hashCode() { return Arrays.hashCode(constraints); } @Override public boolean equals(Object o) { if (o instanceof SubtypeConstraints) { SubtypeConstraints r = (SubtypeConstraints) o; return Arrays.equals(constraints, r.constraints); } else { return false; } } @Override public String toString() { if (constraints == null) { return "⊥"; } else { String r = ""; for (Subtyping.Constraint constraint : constraints) { if (!r.equals("")) { r += ","; } r += constraint; } String c = (candidate == null) ? "_" : candidate.toString(); return "{" + r + "}" + c; } } private Subtyping.Constraints.Solution solve(Subtyping.Constraints.Solution solution) { solution = close(solution, constraints, environment); // Sanity check whether we've finished or not if (!solution.isComplete(nVariables)) { // Solution not satisfiable yet. This maybe because there are unsolved // variables. To resolve these, we have to find "half-open" variables and close // them. Unfortunately, the order in which we do this matters. Therefore, in the // worst case, we may have to try all possible orderings. for (int i = 0; i < nVariables; i++) { Type upper = solution.ceil(i); Type lower = solution.floor(i); boolean u = (upper instanceof Type.Any); boolean l = (lower instanceof Type.Void); // Check for half-open cases which allow an obvious guess. Guessing is critical // for solving some constraint forms. if (!u && l) { Subtyping.Constraints.Solution guess = solve(solution.constrain(i, upper)); if (guess.isComplete(nVariables) && !guess.isUnsatisfiable()) { return guess; } } else if (u && !l) { Subtyping.Constraints.Solution guess = solve(solution.constrain(lower, i)); if (guess.isComplete(nVariables) && !guess.isUnsatisfiable()) { return guess; } } } } return solution; } } // =============================================================================== // Constraint Solving // =============================================================================== /** *

* Apply a given set of symbolic constraints to a given solution until a * fixpoint is reached. For example, consider this set of constraints and * solution: *

* *
	 * { #0 :> #1 }[int|bool :> #0; #1 :> int]
	 * 
* *

* For each constraint, there are two directions of flow: upwards and * downwards. In this case, int|bool flows downwards from * #0 to #1. Likewise, int flows upwards * from #1 to #0. *

* *

* To implement flow in a given direction we employ substitution. For example, * to flow downwards through #0 :> #1 we substitute #0 * for its current upper bound (i.e. int|bool). We then employ the * subtype operator to generate appropriate constraints (or not). In this case, * after substitution we'd have int|bool :> #1 which, in fact, is * the constraint that will be reported. *

* * @param solution * @param constraints * @param subtyping * @param lifetimes * @return */ private static int CLOSING_COUNT = 0; private static Subtyping.Constraints.Solution close(Subtyping.Constraints.Solution solution, Subtyping.Constraint[] constraints, Subtyping.Environment env) { // System.out.println(tab(sTab) + ">>> CLOSING(" + CLOSING_COUNT++ +"): " + solution + " " + Arrays.toString(constraints)); boolean changed = true; int k = 0; // NOTE: this is a very HOT loop on benchmarks with large array // initialCLOSINGisers. // The bound is introduced to prevent against infinite loops. while (changed && k < 10) { changed = false; final Subtyping.Constraints.Solution s = solution; // for (int i = 0; i < constraints.length; ++i) { Subtyping.Constraint ith = constraints[i]; solution = ith.apply(solution); // System.out.println(tab(sTab) + "SOLUTION[" + k + "](" + i + "/" + constraints.length + "): " + ith + " & " + solution + " : " + (s!=solution)); } // Update changed status changed |= (s != solution); k++; } // System.out.println(tab(sTab) + "<<< CLOSING : " + solution); return solution; } /** * Determine whether one declaration is an "ancestor" of another. This happens * in a very specific situation where the child is given the type of the * ancestor with additional constraints. For example: * *
	 * type nat is (int n) where n >= 0
	 * type pos1 is (nat p) where p > 0
	 * type pos2 is (int p) where p > 0
	 * 
* * Here, we'd say that nat is an ancestor of pos1 but * not pos2. * * @param parent * @param child * @return */ private static boolean isAncestorOf(Type parent, Type child) { int t1_opcode = normalise(parent.getOpcode()); int t2_opcode = normalise(child.getOpcode()); if (parent.equals(child)) { // NOTE: seems a bit inefficient as can perform the equality check during this // traversal. return true; } else if (t1_opcode == t2_opcode) { switch (child.getOpcode()) { case TYPE_void: case TYPE_null: case TYPE_bool: case TYPE_byte: case TYPE_int: case TYPE_any: return true; case TYPE_array: { Type.Array p = (Type.Array) parent; Type.Array c = (Type.Array) child; return isAncestorOf(p.getElement(), c.getElement()); } case TYPE_reference: { Type.Reference p = (Type.Reference) parent; Type.Reference c = (Type.Reference) child; // FIXME: what about lifetimes? return p.getElement().equals(c.getElement()); } case TYPE_record: { Type.Record p = (Type.Record) parent; Type.Record c = (Type.Record) child; Tuple p_fields = p.getFields(); Tuple c_fields = c.getFields(); // FIXME: support open records if (p_fields.size() == c_fields.size()) { for (int i = 0; i != p_fields.size(); ++i) { Type.Field f = p_fields.get(i); Type pt = f.getType(); Type ct = c.getField(f.getName()); if (ct == null || !isAncestorOf(pt, ct)) { return false; } } return true; } break; } case TYPE_tuple: { Type.Tuple p = (Type.Tuple) parent; Type.Tuple c = (Type.Tuple) child; // if (p.size() == c.size()) { for (int i = 0; i != p.size(); ++i) { if (!isAncestorOf(p.get(i), c.get(i))) { return false; } } return true; } break; } case TYPE_function: { Type.Function p = (Type.Function) parent; Type.Function c = (Type.Function) child; return isAncestorOf(c.getParameter(), p.getParameter()) && isAncestorOf(p.getReturn(), c.getReturn()); } case TYPE_method: { Type.Method p = (Type.Method) parent; Type.Method c = (Type.Method) child; // FIXME: what about lifetimes? return isAncestorOf(c.getParameter(), p.getParameter()) && isAncestorOf(p.getReturn(), c.getReturn()); } case TYPE_property: { Type.Property p = (Type.Property) parent; Type.Property c = (Type.Property) child; return isAncestorOf(c.getParameter(), p.getParameter()) && isAncestorOf(p.getReturn(), c.getReturn()); } case TYPE_nominal: { Type.Nominal n = (Type.Nominal) child; return isAncestorOf(parent, n.getConcreteType()); } } } else if (t2_opcode == TYPE_nominal) { Type.Nominal n = (Type.Nominal) child; return isAncestorOf(parent, n.getConcreteType()); } return false; } // ================================================================================ // Helpers // ================================================================================ private static final Tuple EMPTY_INVARIANT = new Tuple<>(); /** * Expand a given array whilst using a given element to fill the new spaces. * * @param src * @param n * @param t * @return */ public static Type[] expand(Type[] src, int n, Type t) { final int m = src.length; Type[] nSrc = new Type[n]; System.arraycopy(src, 0, nSrc, 0, m); for (int i = m; i < n; ++i) { nSrc[i] = t; } return nSrc; } /** * Normalise opcode for sake of simplicity. This allows us to compare the types * of two operands more accurately using a switch. * * @param opcode * @return */ public static int normalise(int opcode) { switch (opcode) { case TYPE_method: case TYPE_property: case TYPE_function: return TYPE_function; } // return opcode; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy