wyil.util.Subtyping 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.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import wycc.util.AbstractCompilationUnit.Tuple;
import wyil.lang.WyilFile.Type;
public interface Subtyping {
/**
* Provides an environment in which it makes sense to talk about one type being
* a subtype of another. In particular, such an environment must account for the
* dynamic lifetime graph which is determined by the specific position within a
* given source file.
*
* @author David J. Pearce
*
*/
public interface Environment {
/**
*
* Empty (i.e non-contractive) types are types which cannot accept a value
* because they have an unterminated cycle. An unterminated cycle has no
* leaf nodes terminating it. For example, X<{X field}>
is
* contractive, where as X<{null|X field}>
is not.
*
*
*
* This method returns true if the type is contractive, or contains a
* contractive subcomponent. For example, null|X<{X field}>
is
* considered contractive.
*
*
* @param type --- type to test for contractivity.
* @return
* @throws ResolveError
*/
public boolean isEmpty(QualifiedName nid, Type type);
/**
* Subtract one type from another to produce the result. For example, subtracting
* null
from int|null
produces int
.
*
* @param t1
* @param t2
* @return
*/
public Type subtract(Type t1, Type t2);
/**
*
* Determine whether the rhs
type is a subtype of the
* lhs
(denoted lhs :> rhs
). In the presence of type
* invariants, this operation is undecidable. Therefore, a three-valued
* logic is employed. Either it was concluded that the subtype relation
* definitely holds, or that it definitely does not hold that it
* is unknown whether it holds or not.
*
*
*
* For example, int|null :> int
definitely holds. Likewise,
* int :> int|null
definitely does not hold. However, whether or
* not nat :> pos
holds depends on the type invariants given for
* nat
and pos
which this operator cannot reason
* about. Observe that, in some cases, we do get effective reasoning about types
* with invariants. For example, null|nat :> nat
will be determined
* to definitely hold, despite the fact that nat
has a type
* invariant.
*
*
*
* Depending on the exact language of types involved, this can be a surprisingly
* complex operation. For example, in the presence of union,
* intersection and negation types, the subtype algorithm is
* surprisingly intricate.
*
*
* @param lhs The candidate "supertype". That is, lhs's raw type may be a
* supertype of rhs
's raw type.
* @param rhs The candidate "subtype". That is, rhs's raw type may be a subtype
* of lhs
's raw type.
* @return A given constraints set which may or may not be satisfiable. If the
* constraints are not satisfiable then the relation does not hold.
*/
public Constraints isSubtype(Type lhs, Type rhs);
/**
*
* Determine whether the rhs
type is a subtype of the
* lhs
(denoted lhs :> rhs
). In the presence of type
* invariants, this operation is undecidable. Therefore, a three-valued
* logic is employed. Either it was concluded that the subtype relation
* definitely holds, or that it definitely does not hold that it
* is unknown whether it holds or not.
*
*
*
* For example, int|null :> int
definitely holds. Likewise,
* int :> int|null
definitely does not hold. However, whether or
* not nat :> pos
holds depends on the type invariants given for
* nat
and pos
which this operator cannot reason
* about. Observe that, in some cases, we do get effective reasoning about types
* with invariants. For example, null|nat :> nat
will be determined
* to definitely hold, despite the fact that nat
has a type
* invariant.
*
*
*
* Depending on the exact language of types involved, this can be a surprisingly
* complex operation. For example, in the presence of union,
* intersection and negation types, the subtype algorithm is
* surprisingly intricate.
*
*
* @param lhs The candidate "supertype". That is, lhs's raw type may be a
* supertype of rhs
's raw type.
* @param rhs The candidate "subtype". That is, rhs's raw type may be a subtype
* of lhs
's raw type.
* @return A boolean indicating whether or not one is a subtype of the other.
*/
public boolean isSatisfiableSubtype(Type lhs, Type rhs);
/**
*
* Return the greatest lower bound of two types. That is, for any given types
* T1
and T2
return T3
where
* T1 :> T3
and T2 :> T3
such that no
* T4 :> T3
exists which is also a lower bound of T1
* and T2
. Observe that such a lower bound always exists, as
* void
is a lower bound of any two types. The following
* illustrates some examples:
*
*
*
* int /\ int ============> int
* nat /\ pos ============> void
* bool /\ int ===========> void
* int|null /\ int =======> int
* int|null /\ nat =======> nat
* int|null /\ bool|nat ==> nat
*
*
*
* Here, nat
is (int n) where n >= 0
and
* pos
is (int n) where n > 0
. Observe that, if
* pos
was declared as (nat n) where n >= 0
then
* nat /\ pos ==> pos
.
*
*
* NOTE: If the result equals either of the parameters, then that
* parameter must be returned to allow reference equality to be used to
* determine whether the result matches either parameter.
*
*
* @param lhs
* @param rhs
* @return
*/
public Type greatestLowerBound(Type lhs, Type rhs);
/**
*
* Return the least upper bound of two types. That is, for any given types
* T2
and T3
return T1
where
* T1 :> T3
and T1 :> T2
such that no
* T1 :> T0
exists which is also an upper bound of T2
* and T3
. Observe that such an upper bound always exists, as
* any
is an upper bound of any two types. The following
* illustrates some examples:
*
*
*
* int \/ int ============> int
* int \/ nat ============> int
* nat \/ pos ============> nat|pos
* int \/ null ===========> int|null
* int|null \/ nat =======> null|nat
* int|null \/ bool|nat ==> null|bool|nat
*
*
*
* Here, nat
is (int n) where n >= 0
and
* pos
is (int n) where n > 0
. Observe that, if
* pos
was declared as (nat n) where n >= 0
then
* nat \/ pos ==> nat
.
*
*
* NOTE: If the result equals either of the parameters, then that
* parameter must be returned to allow reference equality to be used to
* determine whether the result matches either parameter.
*
*
* @param lhs
* @param rhs
* @return
*/
public Type leastUpperBound(Type lhs, Type rhs);
/**
*
* Determine, for any two lifetimes l
and m
, whether
* l
is contained within m
or not. This information is
* critical for subtype checking of reference types. Consider this minimal
* example:
*
*
*
* method create() -> (&*:int r):
* return this:new 42
*
*
* This example should not compile. The reason is that the lifetime
* this
is contained within the static lifetime
* *
. Thus, the cell allocated within create()
will be
* deallocated when the method ends and, hence, the method will return a
* dangling reference.
*
*
*
* More generally, an assignment &l:T p = q
is only considered safe
* if it can be shown that the lifetime of the cell referred to by
* p
is within that of q
.
*
*
* @param inner Lifetime which should be enclosed.
* @param outer Lifetime which should be enclosing.
* @author David J. Pearce
* @return
*/
public boolean isWithin(String inner, String outer);
/**
* Declare a given lifetime
* @param inner
* @param outers
* @return
*/
public Environment declareWithin(String inner, String... outers);
}
/**
* represents a set of subtyping constraints which must be satisfiable for a
* given subtyping relationship to hold.
*
* @author David J. Pearce
*
*/
public interface Constraints {
/**
* Determine whether constraints satisfiable or not
*
* @return
*/
public boolean isSatisfiable();
/**
* Get the number of constraints in this set.
*
* @return
*/
public int size();
/**
* Get the largest constraint variable referenced in this constraint set, or
* -1
if none.
*
* @return
*/
public int maxVariable();
/**
* Get the ith constraint in this set.
*
* @param ith
* @return
*/
public Constraint get(int ith);
/**
* Allocate zero or more variables to this constraint set.
*
* @param n
* @return
*/
public Constraints fresh(int n);
/**
* Intersect against one or more subtype constraints.
*
* @param other
* @return
*/
public Constraints intersect(Subtyping.Constraint... other);
/**
* Intersect two sets of subtyping constraints.
*
* @param other
* @return
*/
public Constraints intersect(Constraints other);
/**
* Extract best possible solutions.
*
* @param n
* @return
*/
public Solution solve(int n);
/**
* Represents a solution to a set of subtyping constraints. Each type variable
* has a given lower and upper bound. As the solution evolves, these bounds are
* narrowed done. If we end up where the lower bound is not a subtype of the
* upper bound for some variable, then the solution is invalid.
*
* @author David J. Pearce
*
*/
public interface Solution {
/**
* Return the number of variables which are currently considered in this
* solution.
*
* @return
*/
public int size();
/**
* 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
*/
public boolean isComplete(int n);
/**
* 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
*/
public boolean isUnsatisfiable();
/**
* Get current solution for ith variable.
*
* @param i
* @return
*/
public Type get(int i);
/**
* Get lower bound for given variable.
*
* @param i
* @return
*/
public Type floor(int i);
/**
* Get upper bound for given variable.
*
* @param i
* @return
*/
public Type ceil(int i);
/**
* 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
*/
public Solution constrain(int i, Type nLowerBound);
/**
* 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
*/
public Solution constrain(Type nUpperBound, int i);
}
/**
*
* Represents a matrix of possible typings under consideration. Each environment
* (row) in this matrix represents one possible typing, whilst each column
* represents the possible types for a given (sub)expression. If no rows remain,
* then there are no possible typings and, hence, the original expression is
* untypeable. On the other hand if, at the end, we have more than one possible
* typing then the original expression is ambiguous. For example, consider
* typing this statement:
*
*
*
* int x = ...
* bool y = f(x) == 1
*
*
*
* The expression f(x) + 1
gives rise to typing matrices with four
* columns. One possible matrix might be:
*
*
*
* |0 |1 |2 |3 |
* -+----+----+----+----+
* 0|int |bool|int |bool|
* -+----+----+----+----+
* 1|int |int |int |bool|
* -+----+----+----+----+
*
*
*
* The first column corresponds to the type for subexpression x
.
* Since x
is declared as having int
, every entry is
* int
. The second column corresponds to the type of expression
* f(x)
. In this case, function f()
must be overloaded
* to return bool
in one case and int
in another. The
* third column represents subexpression 1
and the fourth column
* represents the complete expression.
*
*
* In the above, it should be clear that the first row is invalid since we
* cannot compare a boolean with an integer. As such, at some point during
* typing, this row will be "invalidated" (i.e. removed from the set of typings
* under consideration).
*
*
* @author David J Pearce
*
*/
public interface Set {
/**
* Return flag indicating whether there are no valid typings remaining, or not.
* This is equivalent to height() == 0
.
*
* @return
*/
public boolean empty();
/**
* 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
*/
public int height();
/**
* Get the ith row of this typing.
*
* @param ith
* @return
*/
public Subtyping.Constraints get(int ith);
/**
* Attempt to collapse all rows down together using a given comparator. This may
* or may not result in a typing which can be finalised.
*
* @param fn
* @return
*/
public Set fold(Comparator fn);
/**
* 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
*/
public Set map(Function fn);
/**
* 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
*/
public Set project(Function fn);
/**
* Apply a function to each row of the typing matrix, producing an array of
* results
*
* @param fn The mapping function which is applied to each row. This returns
* some element for each row.
* @return
*/
public void foreach(Consumer fn);
}
}
/**
* Represents a single subtyping constraint of the form T1 :> T2
.
* Such constraints can be concrete whether neither bound contains an
* existential variable; or, either bound can contain an existential. For
* example, int|null :> int
is a concrete constraint which holds.
*
* @author David J. Pearce
*
*/
public interface Constraint {
/**
* Return the largest constraint variable referenced in this constraint, or
* -1
if none present.
*
* @return
*/
public int maxVariable();
/**
* Apply this constraint to a given solution producing a potentially updated
* solution.
*
* @param solution
* @return
*/
public Constraints.Solution apply(Constraints.Solution solution);
}
// ===========================================================================
// Constraints
// ===========================================================================
/**
* A simple implementation of a single symboling subtyping constraint.
*
* @author David J. Pearce
*
*/
public static class LowerBoundConstraint implements Subtyping.Constraint {
private final IncrementalSubtypingEnvironment environment;
private final int variable;
private final Type lowerBound;
public LowerBoundConstraint(IncrementalSubtypingEnvironment environment, Type.Existential variable, Type lower) {
if(lower == null || lower instanceof Type.Void) {
throw new IllegalArgumentException("invalid lower bound (" + lower + ")");
}
this.environment = environment;
this.variable = variable.get();
this.lowerBound = lower;
}
@Override
public int maxVariable() {
return Math.max(variable, Subtyping.maxVariable(lowerBound));
}
@Override
public Constraints.Solution apply(Constraints.Solution solution) {
Type cUpper = solution.ceil(variable);
if(!(cUpper instanceof Type.Any || cUpper instanceof Type.Void)) {
Subtyping.Constraints cs = environment.isSubtype(cUpper, lowerBound);
// Propagate information downwards
for(int i=0;i!=cs.size();++i) {
solution = cs.get(i).apply(solution);
}
}
if (lowerBound instanceof Type.Void) {
return solution;
} else {
// Propagate information upwards
Type cLower = substitute(lowerBound, solution, false);
// Here, cLower.isConcrete() guaranteed true
return solution.constrain(variable, cLower);
}
}
@Override
public boolean equals(Object o) {
if(o instanceof LowerBoundConstraint) {
LowerBoundConstraint c = (LowerBoundConstraint) o;
return variable == c.variable && lowerBound.equals(c.lowerBound);
}
return false;
}
@Override
public int hashCode() {
return variable ^ lowerBound.hashCode();
}
@Override
public String toString() {
return "?" + variable + ":>" + lowerBound;
}
}
public static class UpperBoundConstraint implements Subtyping.Constraint {
private final IncrementalSubtypingEnvironment environment;
private final Type upperBound;
private final int variable;
public UpperBoundConstraint(IncrementalSubtypingEnvironment environment, Type upper, Type.Existential variable) {
if(upper == null || upper instanceof Type.Any) {
throw new IllegalArgumentException("invalid upper bound");
}
this.environment = environment;
this.upperBound = upper;
this.variable = variable.get();
}
@Override
public int maxVariable() {
return Math.max(variable, Subtyping.maxVariable(upperBound));
}
@Override
public Constraints.Solution apply(Constraints.Solution solution) {
Type cLower = solution.floor(variable);
if(!(cLower instanceof Type.Void || cLower instanceof Type.Any)) {
Subtyping.Constraints cs = environment.isSubtype(upperBound,cLower);
// Propagate information upwards
for(int i=0;i!=cs.size();++i) {
solution = cs.get(i).apply(solution);
}
}
if (upperBound instanceof Type.Any) {
return solution;
} else {
// Propagate information downwards
Type cUpper = substitute(upperBound, solution, true);
//
return solution.constrain(cUpper, variable);
}
}
@Override
public boolean equals(Object o) {
if(o instanceof UpperBoundConstraint) {
UpperBoundConstraint c = (UpperBoundConstraint) o;
return variable == c.variable && upperBound.equals(c.upperBound);
}
return false;
}
@Override
public int hashCode() {
return variable ^ upperBound.hashCode();
}
@Override
public String toString() {
return upperBound + ":>?" + variable;
}
}
// ===============================================================================
// maxVariable
// ===============================================================================
public static int numberOfVariables(Subtyping.Constraint... constraints) {
int n = 0;
for (int i = 0; i != constraints.length; ++i) {
n = Math.max(constraints[i].maxVariable() + 1, n);
}
return n;
}
/**
* Determine the maximum (existential) type variable used in this type, or -1
if none present.
*
* @param type The type being tested for the presence of existential variables.
* @return
*/
public static int maxVariable(Type type) {
switch (type.getOpcode()) {
case TYPE_any:
case TYPE_bool:
case TYPE_byte:
case TYPE_int:
case TYPE_null:
case TYPE_void:
case TYPE_universal:
return -1;
case TYPE_existential:
return ((Type.Existential)type).get();
case TYPE_array:
return maxVariable(((Type.Array)type).getElement());
case TYPE_reference:
return maxVariable(((Type.Reference)type).getElement());
case TYPE_function:
case TYPE_method:
case TYPE_property: {
Type.Callable t = (Type.Callable) type;
return Math.max(maxVariable(t.getParameter()), maxVariable(t.getReturn()));
}
case TYPE_nominal:
return maxVariable(((Type.Nominal)type).getParameters());
case TYPE_tuple:
return maxVariable(((Type.Tuple)type).getAll());
case TYPE_union:
return maxVariable(((Type.Union)type).getAll());
case TYPE_record: {
Type.Record t = (Type.Record) type;
Tuple fields = t.getFields();
int m = -1;
for (int i = 0; i != fields.size(); ++i) {
m = Math.max(m, maxVariable(fields.get(i).getType()));
}
return m;
}
default:
throw new IllegalArgumentException("unknown type encountered (" + type.getClass().getName() + ")");
}
}
public static int maxVariable(Tuple types) {
int m = -1;
for (int i = 0; i != types.size(); ++i) {
m = Math.max(m, maxVariable(types.get(i)));
}
return m;
}
public static int maxVariable(Type[] types) {
int m = -1;
for (int i = 0; i != types.length; ++i) {
m = Math.max(m, maxVariable(types[i]));
}
return m;
}
// ===============================================================================
// isConcrete
// ===============================================================================
/**
* Check whether a given type is "concrete" or not. That is, whether or not it
* contains a nested existential type variable. For example, the type
* int
does not contain an existential variable! In contrast, the
* type {?1 f}
does. This method performs a fairly straightforward
* recursive descent through the type tree search for existentiuals.
*
* @param type The type being tested for the presence of existential variables.
* @return
*/
public static boolean isConcrete(Type type) {
switch (type.getOpcode()) {
case TYPE_any:
case TYPE_bool:
case TYPE_byte:
case TYPE_int:
case TYPE_null:
case TYPE_void:
case TYPE_universal:
return true;
case TYPE_existential:
return false;
case TYPE_array: {
Type.Array t = (Type.Array) type;
return isConcrete(t.getElement());
}
case TYPE_reference: {
Type.Reference t = (Type.Reference) type;
return isConcrete(t.getElement());
}
case TYPE_function:
case TYPE_method:
case TYPE_property: {
Type.Callable t = (Type.Callable) type;
return isConcrete(t.getParameter()) && isConcrete(t.getReturn());
}
case TYPE_nominal: {
Type.Nominal t = (Type.Nominal) type;
return isConcrete(t.getParameters());
}
case TYPE_tuple: {
Type.Tuple t = (Type.Tuple) type;
return isConcrete(t.getAll());
}
case TYPE_union: {
Type.Union t = (Type.Union) type;
return isConcrete(t.getAll());
}
case TYPE_record: {
Type.Record t = (Type.Record) type;
Tuple fields = t.getFields();
for (int i = 0; i != fields.size(); ++i) {
if (!isConcrete(fields.get(i).getType())) {
return false;
}
}
return true;
}
default:
throw new IllegalArgumentException("unknown type encountered (" + type.getClass().getName() + ")");
}
}
public static boolean isConcrete(Tuple types) {
for (int i = 0; i != types.size(); ++i) {
if (!isConcrete(types.get(i))) {
return false;
}
}
return true;
}
public static boolean isConcrete(Type[] types) {
for (int i = 0; i != types.length; ++i) {
if (!isConcrete(types[i])) {
return false;
}
}
return true;
}
// ================================================================================
// Substitute
// ================================================================================
/**
* Substitute all existential type variables in a given a type in either an
* upper or lower bound position. For example, consider substituting into
* {?0 f}
with a solution int|bool :> ?0 :> int
. In
* the upper position, we end up with {int|bool f}
and in the lower
* position we have {int f}
. A key issue is that positional
* variance must be observed. This applies, for example, to lambda types where
* parameters are contravariant. Thus, consider substituting into
* function(?0)->(?0)
with a solution
* int|bool :> ?0 :> int
. In the upper bound position we get
* function(int)->(int|bool)
, whilst in the lower bound position we
* have function(int|bool)->(int)
.
*
* @param type The type being substituted into.
* @param solution The solution being used for substitution.
* @param sign Indicates the upper (true
) or lower bound
* (false
) position.
* @return
*/
public static Type substitute(Type type, Subtyping.Constraints.Solution solution, boolean sign) {
switch (type.getOpcode()) {
case TYPE_any:
case TYPE_bool:
case TYPE_byte:
case TYPE_int:
case TYPE_null:
case TYPE_void:
case TYPE_universal:
return type;
case TYPE_existential: {
Type.Existential t = (Type.Existential) type;
int var = t.get();
return sign ? solution.ceil(var) : solution.floor(var);
}
case TYPE_array: {
Type.Array t = (Type.Array) type;
Type element = t.getElement();
Type nElement = substitute(element, solution, sign);
if (element == nElement) {
return type;
} else if (nElement instanceof Type.Void) {
return Type.Void;
} else if (nElement instanceof Type.Any) {
return Type.Any;
} else {
return new Type.Array(nElement);
}
}
case TYPE_reference: {
Type.Reference t = (Type.Reference) type;
Type element = t.getElement();
// NOTE: this substitution is effectively a co-variant substitution. Whilst this
// may seem problematic, it isn't because we'll always eliminate variables whose
// bounds are not subtypes of each other. For example, &(int|bool) :> ?1
// :> &(int)
is not satisfiable.
Type nElement = substitute(element, solution, sign);
if (element == nElement) {
return type;
} else if (nElement instanceof Type.Void) {
return Type.Void;
} else if (nElement instanceof Type.Any) {
return Type.Any;
} else {
return new Type.Reference(nElement);
}
}
case TYPE_function:
case TYPE_method:
case TYPE_property: {
Type.Callable t = (Type.Callable) type;
Type parameters = t.getParameter();
Type returns = t.getReturn();
// NOTE: invert sign to account for contra-variance
Type nParameters = substitute(parameters, solution, !sign);
Type nReturns = substitute(returns, solution, sign);
if (nParameters == parameters && nReturns == returns) {
return type;
} else if (nReturns instanceof Type.Void || nParameters instanceof Type.Any) {
return Type.Void;
} else if (nReturns instanceof Type.Any || nParameters instanceof Type.Void) {
return Type.Any;
} else if (type instanceof Type.Function) {
return new Type.Function(nParameters, nReturns);
} else if (type instanceof Type.Property) {
return new Type.Property(nParameters, nReturns);
} else {
Type.Method m = (Type.Method) type;
return new Type.Method(nParameters, nReturns);
}
}
case TYPE_nominal: {
Type.Nominal t = (Type.Nominal) type;
Tuple parameters = t.getParameters();
// NOTE: the following is problematic in the presence of contra-variant
// parameter positions. However, this is not unsound per se. Rather it will just
// mean some variables are eliminated because their bounds are considered
// unsatisfiable.
Tuple nParameters = substitute(parameters, solution, sign);
if (parameters == nParameters) {
return type;
} else {
// Sanity check substitution makes sense
for (int i = 0; i != nParameters.size(); ++i) {
Type ith = nParameters.get(i);
if (ith instanceof Type.Void) {
return Type.Void;
} else if (ith instanceof Type.Any) {
return Type.Any;
}
}
return new Type.Nominal(t.getLink(), nParameters);
}
}
case TYPE_tuple: {
Type.Tuple t = (Type.Tuple) type;
Type[] elements = t.getAll();
Type[] nElements = substitute(elements, solution, sign);
if (elements == nElements) {
return type;
} else {
// Sanity check substitution makes sense
for (int i = 0; i != nElements.length; ++i) {
Type ith = nElements[i];
if (ith instanceof Type.Void) {
return Type.Void;
} else if (ith instanceof Type.Any) {
return Type.Any;
}
}
// Done
return Type.Tuple.create(nElements);
}
}
case TYPE_union: {
Type.Union t = (Type.Union) type;
Type[] elements = t.getAll();
Type[] nElements = substitute(elements, solution, sign);
if (elements == nElements) {
return type;
} else {
// Sanity check substitution makes sense
for (int i = 0; i != nElements.length; ++i) {
Type ith = nElements[i];
if (ith instanceof Type.Void) {
return Type.Void;
} else if (ith instanceof Type.Any) {
return Type.Any;
}
}
// Done
return Type.Union.create(nElements);
}
}
case TYPE_record: {
Type.Record t = (Type.Record) type;
Tuple fields = t.getFields();
Tuple nFields = substituteFields(fields, solution, sign);
if (fields == nFields) {
return type;
} else {
// Sanity check substitution makes sense
for (int i = 0; i != nFields.size(); ++i) {
Type ith = nFields.get(i).getType();
if (ith instanceof Type.Void) {
return Type.Void;
} else if (ith instanceof Type.Any) {
return Type.Any;
}
}
return new Type.Record(t.isOpen(), nFields);
}
}
default:
throw new IllegalArgumentException("unknown type encountered (" + type.getClass().getName() + ")");
}
}
public static Tuple substitute(Tuple types, Subtyping.Constraints.Solution solution, boolean sign) {
for (int i = 0; i != types.size(); ++i) {
Type t = types.get(i);
Type n = substitute(t, solution, sign);
if (t != n) {
// Committed to change
Type[] nTypes = new Type[types.size()];
// Copy all visited so far over
System.arraycopy(types.getAll(), 0, nTypes, 0, i + 1);
// Continue substitution
for (; i < nTypes.length; ++i) {
nTypes[i] = substitute(types.get(i), solution, sign);
}
// Done
return new Tuple<>(nTypes);
}
}
return types;
}
public static Tuple substituteFields(Tuple fields, Subtyping.Constraints.Solution solution,
boolean sign) {
for (int i = 0; i != fields.size(); ++i) {
Type.Field t = fields.get(i);
Type.Field n = substituteField(t, solution, sign);
if (t != n) {
// Committed to change
Type.Field[] nFields = new Type.Field[fields.size()];
// Copy all visited so far over
System.arraycopy(fields.getAll(), 0, nFields, 0, i + 1);
// Continue substitution
for (; i < nFields.length; ++i) {
nFields[i] = substituteField(fields.get(i), solution, sign);
}
// Done
return new Tuple<>(nFields);
}
}
return fields;
}
public static Type.Field substituteField(Type.Field field, Subtyping.Constraints.Solution solution, boolean sign) {
Type type = field.getType();
Type nType = substitute(type, solution, sign);
if (type == nType) {
return field;
} else {
return new Type.Field(field.getName(), nType);
}
}
public static Type[] substitute(Type[] types, Subtyping.Constraints.Solution solution, boolean sign) {
Type[] nTypes = types;
for (int i = 0; i != nTypes.length; ++i) {
Type t = types[i];
Type n = substitute(t, solution, sign);
if (t != n && nTypes == types) {
nTypes = Arrays.copyOf(types, types.length);
}
nTypes[i] = n;
}
return nTypes;
}
}