// Copyright (c) 2011, David J. Pearce (djp@ecs.vuw.ac.nz)
// All rights reserved.
//
// This software may be modified and distributed under the terms
// of the BSD license. See the LICENSE file for details.
package wyc.builder;
import static wybs.lang.SyntaxError.InternalFailure;
import static wyc.lang.WhileyFile.internalFailure;
import static wyc.lang.WhileyFile.syntaxError;
import static wyil.util.ErrorMessages.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
import wyautl_old.lang.Automata;
import wyautl_old.lang.Automaton;
import wybs.lang.*;
import wybs.util.*;
import wyc.lang.*;
import wyc.lang.WhileyFile.Context;
import wycc.util.ArrayUtils;
import wycc.util.Pair;
import wycc.util.Triple;
import wyfs.lang.Path;
import wyfs.util.Trie;
import wyil.lang.Constant;
import wyil.lang.Modifier;
import wyil.lang.Type;
import wyil.lang.WyilFile;
import wyil.util.TypeSystem;
import wyil.util.type.LifetimeRelation;
/**
* Propagates type information in a <i>flow-sensitive</i> fashion from declared
* parameter and return types through variable declarations and assigned
* expressions, to determine types for all intermediate expressions and
* variables. During this propagation, type checking is performed to ensure
* types are used soundly. For example:
*
* <pre>
* function sum(int[] data) -> int:
* int r = 0 // declared int type for r
* for v in data: // infers int type for v, based on type of data
* r = r + v // infers int type for r + v, based on type of operands
* return r // infers int type for return expression
* </pre>
*
* <p>
* The flow typing algorithm distinguishes between the <i>declared type</i> of a
* variable and its <i>known type</i>. That is, the known type at any given
* point is permitted to be more precise than the declared type (but not vice
* versa). For example:
* </p>
*
* <pre>
* function id(int x) -> int:
* return x
*
* function f(int y) -> int:
* int|null x = y
* f(x)
* </pre>
*
* <p>
* The above example is considered type safe because the known type of
* <code>x</code> at the function call is <code>int</code>, which differs from
* its declared type (i.e. <code>int|null</code>).
* </p>
*
* <p>
* Loops present an interesting challenge for type propagation. Consider this
* example:
* </p>
*
* <pre>
* function loopy(int max) -> real:
* var i = 0
* while i < max:
* i = i + 0.5
* return i
* </pre>
*
* <p>
* On the first pass through the loop, variable <code>i</code> is inferred to
* have type <code>int</code> (based on the type of the constant <code>0</code>
* ). However, the add expression is inferred to have type <code>real</code>
* (based on the type of the rhs) and, hence, the resulting type inferred for
* <code>i</code> is <code>real</code>. At this point, the loop must be
* reconsidered taking into account this updated type for <code>i</code>.
* </p>
*
* <p>
* The operation of the flow type checker splits into two stages:
* </p>
* <ul>
* <li><b>Global Propagation.</b> During this stage, all named types are checked
* and expanded.</li>
* <li><b>Local Propagation.</b> During this stage, types are propagated through
* statements and expressions (as above).</li>
* </ul>
*
* <h3>References</h3>
* <ul>
* <li>
* <p>
* David J. Pearce and James Noble. Structural and Flow-Sensitive Types for
* Whiley. Technical Report, Victoria University of Wellington, 2010.
* </p>
* </li>
* </ul>
*
* @author David J. Pearce
*
*/
public class FlowTypeChecker {
private final CompileTask builder;
private final TypeSystem typeSystem;
private WhileyFile file;
// private WhileyFile.FunctionOrMethod current;
/**
* The constant cache contains a cache of expanded constant values. This is
* simply to prevent recomputing them every time.
*/
private final HashMap<NameID, Pair<Constant, Type>> constantCache = new HashMap<>();
public FlowTypeChecker(CompileTask builder) {
this.builder = builder;
this.typeSystem = builder.getTypeSystem();
}
// =========================================================================
// WhileyFile(s)
// =========================================================================
public void propagate(List<WhileyFile> files) {
for (WhileyFile wf : files) {
propagate(wf);
}
}
public void propagate(WhileyFile wf) {
this.file = wf;
for (WhileyFile.Declaration decl : wf.declarations) {
try {
if (decl instanceof WhileyFile.FunctionOrMethodOrProperty) {
propagate((WhileyFile.FunctionOrMethodOrProperty) decl);
} else if (decl instanceof WhileyFile.Type) {
propagate((WhileyFile.Type) decl);
} else if (decl instanceof WhileyFile.Constant) {
propagate((WhileyFile.Constant) decl);
}
} catch (ResolveError e) {
throw new SyntaxError(errorMessage(RESOLUTION_ERROR, e.getMessage()), file.getEntry(), decl, e);
} catch (SyntaxError e) {
throw e;
} catch (Throwable t) {
throw new InternalFailure(t.getMessage(), file.getEntry(), decl, t);
}
}
}
// =========================================================================
// Declarations
// =========================================================================
/**
* Resolve types for a given type declaration. If an invariant expression is
* given, then we have to propagate and resolve types throughout the
* expression.
*
* @param td
* Type declaration to check.
* @throws IOException
*/
public void propagate(WhileyFile.Type td) throws IOException {
try {
// First, resolve the declared syntactic type into the corresponding
// nominal type.
td.resolvedType = builder.toSemanticType(td.parameter.type, td);
if (typeSystem.isSubtype(Type.T_VOID, td.resolvedType)) {
// A non-contractive type is one which cannot accept a finite
// values. For example, the following is a contractive type:
//
// type NonContractive is { NonContractive x }
throw new SyntaxError("empty type encountered", file.getEntry(), td);
} else if (td.invariant.size() > 0) {
// Second, an invariant expression is given, so propagate
// through
// that.
Environment environment = addDeclaredParameter(td.parameter, new Environment(), td);
// Propagate type information through the constraint
for (int i = 0; i != td.invariant.size(); ++i) {
Expr invariant = propagate(td.invariant.get(i), environment, td);
td.invariant.set(i, invariant);
}
}
} catch (ResolveError err) {
throw new SyntaxError(errorMessage(RESOLUTION_ERROR, err.getMessage()), file.getEntry(), td.parameter.type,
err);
}
}
/**
* Propagate and check types for a given constant declaration.
*
* @param cd
* Constant declaration to check.
* @throws IOException
*/
public void propagate(WhileyFile.Constant cd) throws IOException, ResolveError {
NameID nid = new NameID(cd.file().getEntry().id(), cd.name());
cd.resolvedValue = resolveAsConstant(nid).first();
}
/**
* Propagate and check types for a given function or method declaration.
*
* @param fd
* Function or method declaration to check.
* @throws IOException
*/
public void propagate(WhileyFile.FunctionOrMethodOrProperty d) throws IOException {
// Resolve the types of all parameters and construct an appropriate
// environment for use in the flow-sensitive type propagation.
Environment environment = new Environment().declareLifetimeParameters(d.lifetimeParameters);
environment = addDeclaredParameters(d.parameters, environment, d);
environment = addDeclaredParameters(d.returns, environment, d);
// Resolve types for any preconditions (i.e. requires clauses) provided.
propagateConditions(d.requires, environment, d);
// Resolve types for any postconditions (i.e. ensures clauses) provided.
propagateConditions(d.ensures, environment, d);
// Resolve the overall type for the function or method.
if (d instanceof WhileyFile.Function) {
WhileyFile.Function f = (WhileyFile.Function) d;
f.resolvedType = resolveAsType(f.unresolvedType(), d);
} else if(d instanceof WhileyFile.Method) {
WhileyFile.Method m = (WhileyFile.Method) d;
m.resolvedType = resolveAsType(m.unresolvedType(), d);
} else {
WhileyFile.Property m = (WhileyFile.Property) d;
m.resolvedType = resolveAsType(m.unresolvedType(), d);
}
// Add the "this" lifetime
environment = environment.startNamedBlock("this");
// Finally, propagate type information throughout all statements in the
// function / method body.
Environment last = propagate(d.statements, environment, d);
//
checkReturnValue(d, last);
}
/**
* Check that a return value is provided when it is needed. For example, a
* return value is not required for a method that has no return type.
* Likewise, we don't expect one from a native method since there was no
* body to analyse.
*
* @param d
* @param last
*/
private void checkReturnValue(WhileyFile.FunctionOrMethodOrProperty d, Environment last) {
if (!d.hasModifier(Modifier.NATIVE) && last != BOTTOM && d.resolvedType().returns().length != 0
&& !(d instanceof WhileyFile.Property)) {
// In this case, code reaches the end of the function or method and,
// furthermore, that this requires a return value. To get here means
// that there was no explicit return statement given on at least one
// execution path.
throw new SyntaxError("missing return statement", file.getEntry(), d);
}
}
/**
* Propagate type information through a list of conditions, updating each
* one in place. The environment is cloned so as to ensure no interference.
*
* @param conditions
* @param environment
* @param context
*/
private void propagateConditions(List<Expr> conditions, Environment environment, Context context) {
for (int i = 0; i != conditions.size(); ++i) {
Expr condition = conditions.get(i);
condition = propagate(condition, environment.clone(), context);
conditions.set(i, condition);
}
}
// =========================================================================
// Blocks & Statements
// =========================================================================
/**
* Propagate type information in a flow-sensitive fashion through a block of
* statements, whilst type checking each statement and expression.
*
* @param block
* Block of statements to flow sensitively type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(ArrayList<Stmt> block, Environment environment, Context context) {
for (int i = 0; i != block.size(); ++i) {
Stmt stmt = block.get(i);
if (stmt instanceof Expr) {
block.set(i, (Stmt) propagate((Expr) stmt, environment, context));
} else {
environment = propagate(stmt, environment, context);
}
}
return environment;
}
/**
* Propagate type information in a flow-sensitive fashion through a given
* statement, whilst type checking it at the same time. For statements which
* contain other statements (e.g. if, while, etc), then this will
* recursively propagate type information through them as well.
*
*
* @param forest
* Block of statements to flow-sensitively type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt stmt, Environment environment, Context context) {
if (environment == BOTTOM) {
throw new SyntaxError(errorMessage(UNREACHABLE_CODE), file.getEntry(), stmt);
}
try {
if (stmt instanceof Stmt.VariableDeclaration) {
return propagate((Stmt.VariableDeclaration) stmt, environment, context);
} else if (stmt instanceof Stmt.Assign) {
return propagate((Stmt.Assign) stmt, environment, context);
} else if (stmt instanceof Stmt.Return) {
return propagate((Stmt.Return) stmt, environment, context);
} else if (stmt instanceof Stmt.IfElse) {
return propagate((Stmt.IfElse) stmt, environment, context);
} else if (stmt instanceof Stmt.NamedBlock) {
return propagate((Stmt.NamedBlock) stmt, environment, context);
} else if (stmt instanceof Stmt.While) {
return propagate((Stmt.While) stmt, environment, context);
} else if (stmt instanceof Stmt.Switch) {
return propagate((Stmt.Switch) stmt, environment, context);
} else if (stmt instanceof Stmt.DoWhile) {
return propagate((Stmt.DoWhile) stmt, environment, context);
} else if (stmt instanceof Stmt.Break) {
return propagate((Stmt.Break) stmt, environment, context);
} else if (stmt instanceof Stmt.Continue) {
return propagate((Stmt.Continue) stmt, environment, context);
} else if (stmt instanceof Stmt.Assert) {
return propagate((Stmt.Assert) stmt, environment, context);
} else if (stmt instanceof Stmt.Assume) {
return propagate((Stmt.Assume) stmt, environment, context);
} else if (stmt instanceof Stmt.Fail) {
return propagate((Stmt.Fail) stmt, environment, context);
} else if (stmt instanceof Stmt.Debug) {
return propagate((Stmt.Debug) stmt, environment, context);
} else if (stmt instanceof Stmt.Skip) {
return propagate((Stmt.Skip) stmt, environment);
} else {
throw new InternalFailure("unknown statement: " + stmt.getClass().getName(), file.getEntry(), stmt);
}
} catch (ResolveError e) {
throw new SyntaxError(errorMessage(RESOLUTION_ERROR, e.getMessage()), file.getEntry(), stmt, e);
} catch (SyntaxError e) {
throw e;
} catch (Throwable e) {
throw new InternalFailure(e.getMessage(), file.getEntry(), stmt, e);
}
}
/**
* Type check an assertion statement. This requires checking that the
* expression being asserted is well-formed and has boolean type.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
* @throws ResolveError
* If a named type within this statement cannot be resolved
* within the enclosing project.
*/
private Environment propagate(Stmt.Assert stmt, Environment environment, Context context) throws ResolveError {
stmt.expr = propagate(stmt.expr, environment, context);
checkIsSubtype(Type.T_BOOL, stmt.expr, environment);
return environment;
}
/**
* Type check an assume statement. This requires checking that the
* expression being asserted is well-formed and has boolean type.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
* @throws ResolveError
* If a named type within this statement cannot be resolved
* within the enclosing project.
*/
private Environment propagate(Stmt.Assume stmt, Environment environment, Context context) throws ResolveError {
stmt.expr = propagate(stmt.expr, environment, context);
checkIsSubtype(Type.T_BOOL, stmt.expr, environment);
return environment;
}
/**
* Type check a fail statement. The environment after a fail statement is
* "bottom" because that represents an unreachable program point.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.Fail stmt, Environment environment, Context context) {
return BOTTOM;
}
/**
* Type check a variable declaration statement. This must associate the
* given variable with either its declared and actual type in the
* environment. If no initialiser is given, then the actual type is the void
* (since the variable is not yet defined). Otherwise, the actual type is
* the type of the initialiser expression. Additionally, when an initialiser
* is given we must check it is well-formed and that it is a subtype of the
* declared type.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.VariableDeclaration stmt, Environment environment, Context context)
throws IOException, ResolveError {
// First, resolve declared type
stmt.type = builder.toSemanticType(stmt.parameter.type, context);
// Second, resolve type of initialiser. This must be performed before we
// update the environment, since this expression is not allowed to refer
// to the newly declared variable.
if (stmt.expr != null) {
stmt.expr = propagate(stmt.expr, environment, context);
checkIsSubtype(stmt.type, stmt.expr, environment);
}
// Third, update environment accordingly. Observe that we can safely
// assume any variable(s) are not already declared in the enclosing
// scope because the parser checks this for us.
environment = addDeclaredParameter(stmt.parameter, environment, context);
// Done.
return environment;
}
/**
* Type check an assignment statement.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.Assign stmt, Environment environment, Context context)
throws IOException, ResolveError {
// First, type check each lval that occurs on the left-hand side.
for (int i = 0; i != stmt.lvals.size(); ++i) {
stmt.lvals.set(i, propagate(stmt.lvals.get(i), environment, context));
}
// Second, type check expressions on right-hand side, and calculate the
// number of values produced by the right-hand side. This is challenging
// because the number of explicit rvals given can legitimately be less
// than the number of values produced. This occurs when an invocation
// occurs on the right-hand side has multiple return values.
for (int i = 0; i != stmt.rvals.size(); ++i) {
stmt.rvals.set(i, propagate(stmt.rvals.get(i), environment, context));
}
List<Pair<Expr, Type>> valuesProduced = calculateTypesProduced(stmt.rvals);
// Check the number of expected values matches the number of values
// produced by the right-hand side.
if (stmt.lvals.size() < valuesProduced.size()) {
throw new SyntaxError("too many values provided on right-hand side", file.getEntry(), stmt);
} else if (stmt.lvals.size() > valuesProduced.size()) {
throw new SyntaxError("not enough values provided on right-hand side", file.getEntry(), stmt);
}
// For each value produced, check that the variable being assigned
// matches the value produced.
for (int i = 0; i != valuesProduced.size(); ++i) {
Expr.LVal lval = stmt.lvals.get(i);
Pair<Expr, Type> rval = valuesProduced.get(i);
checkIsSubtype(getWriteableType(lval, environment), rval.second(), rval.first(), environment);
}
return environment;
}
/**
* Determine the maximal type that can be written to this given lval. For
* example, if the lval is just a variable then the declared type is the
* writeable type.
*
* @param lv
* @param environment
* @return
*/
private Type getWriteableType(Expr.LVal lv, Environment environment) {
if (lv instanceof Expr.AssignedVariable) {
Expr.AssignedVariable v = (Expr.AssignedVariable) lv;
return environment.getDeclaredType(v.var);
} else if (lv instanceof Expr.Dereference) {
Expr.Dereference pa = (Expr.Dereference) lv;
return pa.srcType.getWriteableElementType();
} else if (lv instanceof Expr.IndexOf) {
Expr.IndexOf la = (Expr.IndexOf) lv;
return la.srcType.getWriteableElementType();
} else if (lv instanceof Expr.FieldAccess) {
Expr.FieldAccess la = (Expr.FieldAccess) lv;
return la.srcType.getWriteableFieldType(la.name);
} else {
throw new InternalFailure("unknown lval: " + lv.getClass().getName(), file.getEntry(), lv);
}
}
/**
* Type check a break statement. This requires propagating the current
* environment to the block destination, to ensure that the actual types of
* all variables at that point are precise.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.Break stmt, Environment environment, Context context) {
// FIXME: need to propagate environment to the break destination
return BOTTOM;
}
/**
* Type check a continue statement. This requires propagating the current
* environment to the block destination, to ensure that the actual types of
* all variables at that point are precise.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.Continue stmt, Environment environment, Context context) {
// FIXME: need to propagate environment to the continue destination
return BOTTOM;
}
/**
* Type check an assume statement. This requires checking that the
* expression being printed is well-formed and has string type.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
* @throws ResolveError
*/
private Environment propagate(Stmt.Debug stmt, Environment environment, Context context) throws ResolveError {
stmt.expr = propagate(stmt.expr, environment, context);
checkIsSubtype(Type.Array(Type.T_INT), stmt.expr, environment);
return environment;
}
/**
* Type check a do-while statement.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
* @throws ResolveError
* If a named type within this statement cannot be resolved
* within the enclosing project.
*/
private Environment propagate(Stmt.DoWhile stmt, Environment environment, Context context) throws ResolveError {
// Type loop body
environment = propagate(stmt.body, environment, context);
// Type invariants
List<Expr> stmt_invariants = stmt.invariants;
for (int i = 0; i != stmt_invariants.size(); ++i) {
Expr invariant = stmt_invariants.get(i);
invariant = propagate(invariant, environment, context);
stmt_invariants.set(i, invariant);
checkIsSubtype(Type.T_BOOL, invariant, environment);
}
// Type condition assuming its false to represent the terminated loop.
// This is important if the condition contains a type test, as we'll
// know that doesn't hold here.
Pair<Expr, Environment> p = propagateCondition(stmt.condition, false, environment, context);
stmt.condition = p.first();
return p.second();
}
/**
* Type check an if-statement. To do this, we propagate the environment
* through both sides of condition expression. Each can produce a different
* environment in the case that runtime type tests are used. These
* potentially updated environments are then passed through the true and
* false blocks which, in turn, produce updated environments. Finally, these
* two environments are joined back together. The following illustrates:
*
* <pre>
* // Environment
* function f(int|null x) -> int:
* // {x : int|null}
* if x is null:
* // {x : null}
* x = 0
* // {x : int}
* else:
* // {x : int}
* x = x + 1
* // {x : int}
* // --------------------------------------------------
* // {x : int} o {x : int} => {x : int}
* return x
* </pre>
*
* Here, we see that the type of <code>x</code> is initially
* <code>int|null</code> before the first statement of the function body. On
* the true branch of the type test this is updated to <code>null</code>,
* whilst on the false branch it is updated to <code>int</code>. Finally,
* the type of <code>x</code> at the end of each block is <code>int</code>
* and, hence, its type after the if-statement is <code>int</code>.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
* @throws ResolveError
* If a named type within this statement cannot be resolved
* within the enclosing project.
*/
private Environment propagate(Stmt.IfElse stmt, Environment environment, Context context) throws ResolveError {
// First, check condition and apply variable retypings.
Pair<Expr, Environment> p1, p2;
p1 = propagateCondition(stmt.condition, true, environment.clone(), context);
p2 = propagateCondition(stmt.condition, false, environment.clone(), context);
stmt.condition = p1.first();
Environment trueEnvironment = p1.second();
Environment falseEnvironment = p2.second();
// Second, update environments for true and false branches
if (stmt.trueBranch != null && stmt.falseBranch != null) {
trueEnvironment = propagate(stmt.trueBranch, trueEnvironment, context);
falseEnvironment = propagate(stmt.falseBranch, falseEnvironment, context);
} else if (stmt.trueBranch != null) {
trueEnvironment = propagate(stmt.trueBranch, trueEnvironment, context);
} else if (stmt.falseBranch != null) {
trueEnvironment = environment;
falseEnvironment = propagate(stmt.falseBranch, falseEnvironment, context);
}
// Finally, join results back together
return trueEnvironment.merge(environment.keySet(), falseEnvironment);
}
/**
* Type check a <code>return</code> statement. If a return expression is
* given, then we must check that this is well-formed and is a subtype of
* the enclosing function or method's declared return type. The environment
* after a return statement is "bottom" because that represents an
* unreachable program point.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
* @throws ResolveError
* If a named type within this statement cannot be resolved
* within the enclosing project.
*/
private Environment propagate(Stmt.Return stmt, Environment environment, Context context)
throws IOException, ResolveError {
List<Expr> stmt_returns = stmt.returns;
for (int i = 0; i != stmt_returns.size(); ++i) {
stmt_returns.set(i, propagate(stmt_returns.get(i), environment, context));
}
List<Pair<Expr, Type>> stmt_types = calculateTypesProduced(stmt_returns);
// FIXME: this is less than ideal
Type[] current_returns = ((WhileyFile.FunctionOrMethodOrProperty) context).resolvedType().returns();
if (stmt_types.size() < current_returns.length) {
// In this case, a return statement was provided with too few return
// values compared with the number declared for the enclosing
// method.
throw new SyntaxError("not enough return values provided", file.getEntry(), stmt);
} else if (stmt_types.size() > current_returns.length) {
// In this case, a return statement was provided with too many
// return
// values compared with the number declared for the enclosing
// method.
throw new SyntaxError("too many return values provided", file.getEntry(), stmt);
}
// Number of return values match number declared for enclosing
// function/method. Now, check they have appropriate types.
for (int i = 0; i != current_returns.length; ++i) {
Pair<Expr, Type> p = stmt_types.get(i);
Type t = current_returns[i];
checkIsSubtype(t, p.second(), p.first(), environment);
}
environment.free();
return BOTTOM;
}
/**
* Type check a <code>skip</code> statement, which has no effect on the
* environment.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.Skip stmt, Environment environment) {
return environment;
}
/**
* Type check a <code>switch</code> statement. This is similar, in some
* ways, to the handling of if-statements except that we have n code blocks
* instead of just two. Therefore, we propagate type information through
* each block, which produces n potentially different environments and these
* are all joined together to produce the environment which holds after this
* statement. For example:
*
* <pre>
* // Environment
* function f(int x) -> int|null:
* int|null y
* // {x : int, y : void}
* switch x:
* case 0:
* // {x : int, y : void}
* return 0
* // { }
* case 1,2,3:
* // {x : int, y : void}
* y = x
* // {x : int, y : int}
* default:
* // {x : int, y : void}
* y = null
* // {x : int, y : null}
* // --------------------------------------------------
* // {} o
* // {x : int, y : int} o
* // {x : int, y : null}
* // => {x : int, y : int|null}
* return y
* </pre>
*
* Here, the environment after the declaration of <code>y</code> has its
* actual type as <code>void</code> since no value has been assigned yet.
* For each of the case blocks, this initial environment is (separately)
* updated to produce three different environments. Finally, each of these
* is joined back together to produce the environment going into the
* <code>return</code> statement.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.Switch stmt, Environment environment, Context context) throws IOException {
stmt.expr = propagate(stmt.expr, environment, context);
Environment finalEnv = null;
boolean hasDefault = false;
for (Stmt.Case c : stmt.cases) {
// first, resolve the constants
ArrayList<Constant> values = new ArrayList<>();
for (Expr e : c.expr) {
values.add(resolveAsConstant(e, context).first());
}
c.constants = values;
// second, propagate through the statements
Environment localEnv = environment.clone();
localEnv = propagate(c.stmts, localEnv, context);
if (finalEnv == null) {
finalEnv = localEnv;
} else {
finalEnv = finalEnv.merge(environment.keySet(), localEnv);
}
// third, keep track of whether a default
hasDefault |= c.expr.isEmpty();
}
if (!hasDefault) {
// in this case, there is no default case in the switch. We must
// therefore assume that there are values which will fall right
// through the switch statement without hitting a case. Therefore,
// we must include the original environment to accound for this.
finalEnv = finalEnv.merge(environment.keySet(), environment);
} else {
environment.free();
}
return finalEnv;
}
/**
* Type check a <code>NamedBlock</code> statement.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
*/
private Environment propagate(Stmt.NamedBlock stmt, Environment environment, Context context) {
environment = environment.startNamedBlock(stmt.name);
environment = propagate(stmt.body, environment, context);
return environment.endNamedBlock(stmt.name);
}
/**
* Type check a <code>whiley</code> statement.
*
* @param stmt
* Statement to type check
* @param environment
* Determines the type of all variables immediately going into
* this block
* @return
* @throws ResolveError
* If a named type within this statement cannot be resolved
* within the enclosing project.
*/
private Environment propagate(Stmt.While stmt, Environment environment, Context context) throws ResolveError {
// Type condition assuming its false to represent the terminated loop.
// This is important if the condition contains a type test, as we'll
// know that doesn't hold here.
Pair<Expr, Environment> p1 = propagateCondition(stmt.condition, true, environment.clone(), context);
Pair<Expr, Environment> p2 = propagateCondition(stmt.condition, false, environment.clone(), context);
stmt.condition = p1.first();
Environment trueEnvironment = p1.second();
Environment falseEnvironment = p2.second();
// Type loop invariant(s)
List<Expr> stmt_invariants = stmt.invariants;
for (int i = 0; i != stmt_invariants.size(); ++i) {
Expr invariant = stmt_invariants.get(i);
invariant = propagate(invariant, environment, context);
stmt_invariants.set(i, invariant);
checkIsSubtype(Type.T_BOOL, invariant, environment);
}
// Type loop body
propagate(stmt.body, trueEnvironment, context);
// Done
return falseEnvironment;
}
// =========================================================================
// LVals
// =========================================================================
private Expr.LVal propagate(Expr.LVal lval, Environment environment, Context context) {
try {
if (lval instanceof Expr.AbstractVariable) {
Expr.AbstractVariable av = (Expr.AbstractVariable) lval;
Type p = environment.getCurrentType(av.var);
if (p == null) {
throw new SyntaxError(errorMessage(UNKNOWN_VARIABLE), file.getEntry(), lval);
}
Expr.AssignedVariable lv = new Expr.AssignedVariable(av.var, av.attributes());
lv.type = p;
return lv;
} else if (lval instanceof Expr.Dereference) {
Expr.Dereference pa = (Expr.Dereference) lval;
Expr.LVal src = propagate((Expr.LVal) pa.src, environment, context);
pa.src = src;
pa.srcType = expandAsEffectiveReference(src, context);
return pa;
} else if (lval instanceof Expr.IndexOf) {
// this indicates either a list, string or dictionary update
Expr.IndexOf ai = (Expr.IndexOf) lval;
Expr.LVal src = propagate((Expr.LVal) ai.src, environment, context);
Expr index = propagate(ai.index, environment, context);
ai.src = src;
ai.index = index;
ai.srcType = expandAsEffectiveArray(src, context);
return ai;
} else if (lval instanceof Expr.FieldAccess) {
// this indicates a record update
Expr.FieldAccess ad = (Expr.FieldAccess) lval;
Expr.LVal src = propagate((Expr.LVal) ad.src, environment, context);
Expr.FieldAccess ra = new Expr.FieldAccess(src, ad.name, ad.attributes());
Type.EffectiveRecord srcType = expandAsEffectiveRecord(src, context);
if (!srcType.hasField(ra.name)) {
throw new SyntaxError(errorMessage(RECORD_MISSING_FIELD, ra.name), file.getEntry(), lval);
}
ra.srcType = srcType;
return ra;
}
} catch (SyntaxError e) {
throw e;
} catch (Throwable e) {
throw new InternalFailure(e.getMessage(), file.getEntry(), lval, e);
}
throw new InternalFailure("unknown lval: " + lval.getClass().getName(), file.getEntry(), lval);
}
// =========================================================================
// Condition
// =========================================================================
/**
* <p>
* Propagate type information through an expression being used as a
* condition, whilst checking it is well-typed at the same time. When used
* as a condition (e.g. of an if-statement) an expression may update the
* environment in accordance with any type tests used within. This is
* important to ensure that variables are retyped in e.g. if-statements. For
* example:
* </p>
*
* <pre>
* if x is int && x >= 0
* // x is int
* else:
* //
* </pre>
* <p>
* Here, the if-condition must update the type of x in the true branch, but
* *cannot* update the type of x in the false branch.
* </p>
* <p>
* To handle conditions on the false branch, this function uses a sign flag
* rather than expanding them using DeMorgan's laws (for efficiency). When
* determining type for the false branch, the sign flag is initially false.
* This prevents falsely concluding that e.g. "x is int" holds in the false
* branch.
* </p>
*
* @param expr
* Condition expression to type check and propagate through
* @param sign
* Indicates how expression should be treated. If true, then
* expression is treated "as is"; if false, then expression
* should be treated as negated
* @param environment
* Determines the type of all variables immediately going into
* this expression
* @param context
* Enclosing context of this expression (e.g. type declaration,
* function declaration, etc)
* @return
* @throws ResolveError
* If a named type within this condition cannot be resolved
* within the enclosing project.
*/
public Pair<Expr, Environment> propagateCondition(Expr expr, boolean sign, Environment environment, Context context)
throws ResolveError {
// Split up into the compound and non-compound forms.
if (expr instanceof Expr.UnOp) {
return propagateCondition((Expr.UnOp) expr, sign, environment, context);
} else if (expr instanceof Expr.BinOp) {
return propagateCondition((Expr.BinOp) expr, sign, environment, context);
} else {
// For non-compound forms, can just default back to the base rules
// for general expressions.
expr = propagate(expr, environment, context);
checkIsSubtype(Type.T_BOOL, expr, context, environment);
return new Pair<>(expr, environment);
}
}
/**
* <p>
* Propagate type information through a unary expression being used as a
* condition and, in fact, only logical not is syntactically valid here.
* </p>
*
* @param expr
* Condition expression to type check and propagate through
* @param sign
* Indicates how expression should be treated. If true, then
* expression is treated "as is"; if false, then expression
* should be treated as negated
* @param environment
* Determines the type of all variables immediately going into
* this expression
* @param context
* Enclosing context of this expression (e.g. type declaration,
* function declaration, etc)
* @return
* @throws ResolveError
* If a named type within this condition cannot be resolved
* within the enclosing project.
*/
private Pair<Expr, Environment> propagateCondition(Expr.UnOp expr, boolean sign, Environment environment,
Context context) throws ResolveError {
Expr.UnOp uop = expr;
// Check whether we have logical not
if (uop.op == Expr.UOp.NOT) {
Pair<Expr, Environment> p = propagateCondition(uop.mhs, !sign, environment, context);
uop.mhs = p.first();
checkIsSubtype(Type.T_BOOL, uop.mhs, context, environment);
uop.type = Type.T_BOOL;
return new Pair<>(uop, p.second());
} else {
// Nothing else other than logical not is valid at this point.
syntaxError(errorMessage(INVALID_BOOLEAN_EXPRESSION), context, expr);
return deadCode(expr);
}
}
/**
* <p>
* Propagate type information through a binary expression being used as a
* condition. In this case, only logical connectives ("&&", "||", "^") and
* comparators (e.g. "==", "<=", etc) are permitted here.
* </p>
*
* @param expr
* Condition expression to type check and propagate through
* @param sign
* Indicates how expression should be treated. If true, then
* expression is treated "as is"; if false, then expression
* should be treated as negated
* @param environment
* Determines the type of all variables immediately going into
* this expression
* @param context
* Enclosing context of this expression (e.g. type declaration,
* function declaration, etc)
* @return
* @throws ResolveError
* If a named type within this condition cannot be resolved
* within the enclosing project.
*/
private Pair<Expr, Environment> propagateCondition(Expr.BinOp bop, boolean sign, Environment environment,
Context context) throws ResolveError {
Expr.BOp op = bop.op;
// Split into the two broard cases: logical connectives and primitives.
switch (op) {
case AND:
case OR:
case XOR:
return resolveNonLeafCondition(bop, sign, environment, context);
case EQ:
case NEQ:
case LT:
case LTEQ:
case GT:
case GTEQ:
case IS:
return resolveLeafCondition(bop, sign, environment, context);
default:
syntaxError(errorMessage(INVALID_BOOLEAN_EXPRESSION), context, bop);
return null; // dead code
}
}
/**
* <p>
* Propagate type information through a binary expression being used as a
* logical connective ("&&", "||", "^").
* </p>
*
* @param bop
* Binary operator for this expression.
* @param sign
* Indicates how expression should be treated. If true, then
* expression is treated "as is"; if false, then expression
* should be treated as negated
* @param environment
* Determines the type of all variables immediately going into
* this expression
* @param context
* Enclosing context of this expression (e.g. type declaration,
* function declaration, etc)
* @return
* @throws ResolveError
* If a named type within this condition cannot be resolved
* within the enclosing project.
*/
private Pair<Expr, Environment> resolveNonLeafCondition(Expr.BinOp bop, boolean sign, Environment environment,
Context context) throws ResolveError {
Expr.BOp op = bop.op;
Pair<Expr, Environment> p;
boolean followOn = (sign && op == Expr.BOp.AND) || (!sign && op == Expr.BOp.OR);
if (followOn) {
// In this case, the environment feeds directly from the result of
// propagating through the lhs into the rhs, and then into the
// result of this expression. This means that updates to the
// environment by either the lhs or rhs are visible outside of this
// method.
p = propagateCondition(bop.lhs, sign, environment.clone(), context);
bop.lhs = p.first();
p = propagateCondition(bop.rhs, sign, p.second(), context);
bop.rhs = p.first();
environment = p.second();
} else {
// We could do better here
p = propagateCondition(bop.lhs, sign, environment.clone(), context);
bop.lhs = p.first();
Environment local = p.second();
// Recompute the lhs assuming that it is false. This is necessary to
// generate the right environment going into the rhs, which is only
// evaluated if the lhs is false. For example:
//
// if(e is int && e > 0):
// //
// else:
// // <-
//
// In the false branch, we're determing the environment for
// !(e is int && e > 0). This becomes !(e is int) || (e <= 0) where
// on the rhs we require (e is int).
p = propagateCondition(bop.lhs, !sign, environment.clone(), context);
// Note, the following is intentional since we're specifically
// considering the case where the lhs was false, and this case is
// true.
p = propagateCondition(bop.rhs, sign, p.second(), context);
bop.rhs = p.first();
environment = local.merge(local.keySet(), p.second());
}
checkIsSubtype(Type.T_BOOL, bop.lhs, context, environment);
checkIsSubtype(Type.T_BOOL, bop.rhs, context, environment);
bop.srcType = Type.T_BOOL;
return new Pair<>(bop, environment);
}
/**
* <p>
* Propagate type information through a binary expression being used as a
* comparators (e.g. "==", "<=", etc).
* </p>
*
* @param bop
* Binary operator for this expression.
* @param sign
* Indicates how expression should be treated. If true, then
* expression is treated "as is"; if false, then expression
* should be treated as negated
* @param environment
* Determines the type of all variables immediately going into
* this expression
* @param context
* Enclosing context of this expression (e.g. type declaration,
* function declaration, etc)
* @return
* @throws ResolveError
* If a named type within this condition cannot be resolved
* within the enclosing project.
*/
private Pair<Expr, Environment> resolveLeafCondition(Expr.BinOp bop, boolean sign, Environment environment,
Context context) throws ResolveError {
Expr.BOp op = bop.op;
Expr lhs = propagate(bop.lhs, environment, context);
Expr rhs = propagate(bop.rhs, environment, context);
bop.lhs = lhs;
bop.rhs = rhs;
Type lhsType = lhs.result();
Type rhsType = rhs.result();
switch (op) {
case IS:
// this one is slightly more difficult. In the special case that
// we have a type constant on the right-hand side then we want
// to check that it makes sense. Otherwise, we just check that
// it has type meta.
if (rhs instanceof Expr.TypeVal) {
// yes, right-hand side is a constant
Expr.TypeVal tv = (Expr.TypeVal) rhs;
Type glbForFalseBranch = Type.Intersection(lhs.result(), Type.Negation(tv.type));
Type glbForTrueBranch = Type.Intersection(lhs.result(), tv.type);
if (typeSystem.isEmpty(glbForFalseBranch)) {
// DEFINITE TRUE CASE
syntaxError(errorMessage(BRANCH_ALWAYS_TAKEN), context, bop);
} else if (typeSystem.isEmpty(glbForTrueBranch)) {
// DEFINITE FALSE CASE
syntaxError(errorMessage(INCOMPARABLE_OPERANDS, lhsType, tv.type), context, bop);
}
// Finally, if the lhs is local variable then update its
// type in the resulting environment.
if (lhs instanceof Expr.LocalVariable) {
Expr.LocalVariable lv = (Expr.LocalVariable) lhs;
Type newType;
if (sign) {
newType = glbForTrueBranch;
} else {
newType = glbForFalseBranch;
}
environment = environment.update(lv.var, newType);
}
} else {
// In this case, we can't update the type of the lhs since
// we don't know anything about the rhs. It may be possible
// to support bounds here in order to do that, but frankly
// that's future work :)
checkIsSubtype(Type.T_META, rhs, context, environment);
}
bop.srcType = lhs.result();
break;
case LT:
case LTEQ:
case GTEQ:
case GT:
checkSuptypes(lhs, context, environment, Type.T_INT);
checkSuptypes(rhs, context, environment, Type.T_INT);
//
if (typeSystem.isSubtype(lhsType, rhsType) || typeSystem.isSubtype(rhsType, lhsType)) {
bop.srcType = lhs.result();
} else {
throw new SyntaxError(errorMessage(INCOMPARABLE_OPERANDS, lhsType, rhsType), file.getEntry(), bop);
}
break;
case NEQ:
// following is a sneaky trick for the special case below
sign = !sign;
case EQ:
// first, check for special case of e.g. x != null. This is then
// treated the same as !(x is null)
if (lhs instanceof Expr.LocalVariable && rhs instanceof Expr.Constant
&& ((Expr.Constant) rhs).value == Constant.Null) {
// bingo, special case
Expr.LocalVariable lv = (Expr.LocalVariable) lhs;
Type newType;
Type glb = Type.Intersection(lhs.result(), Type.T_NULL);
if (typeSystem.isEmpty(glb)) {
syntaxError(errorMessage(INCOMPARABLE_OPERANDS, lhs.result(), Type.T_NULL), context, bop);
return null;
} else if (sign) {
newType = glb;
} else {
newType = Type.Intersection(lhs.result(), Type.Negation(Type.T_NULL));
}
bop.srcType = lhs.result();
environment = environment.update(lv.var, newType);
} else {
// handle general case
if (typeSystem.isSubtype(lhsType, rhsType, environment.getLifetimeRelation())) {
bop.srcType = lhs.result();
} else if (typeSystem.isSubtype(rhsType, lhsType, environment.getLifetimeRelation())) {
bop.srcType = rhs.result();
} else {
syntaxError(errorMessage(INCOMPARABLE_OPERANDS, lhsType, rhsType), context, bop);
return null; // dead code
}
}
}
return new Pair<>(bop, environment);
}
// =========================================================================
// Expressions
// =========================================================================
/**
* Propagate types through a given expression, whilst checking that it is
* well typed. In this case, any use of a runtime type test cannot effect
* callers of this function.
*
* @param expr
* Expression to propagate types through.
* @param environment
* Determines the type of all variables immediately going into
* this expression
* @param context
* Enclosing context of this expression (e.g. type declaration,
* function declaration, etc)
* @return
*/
public Expr propagate(Expr expr, Environment environment, Context context) {
try {
if (expr instanceof Expr.BinOp) {
return propagate((Expr.BinOp) expr, environment, context);
} else if (expr instanceof Expr.UnOp) {
return propagate((Expr.UnOp) expr, environment, context);
} else if (expr instanceof Expr.Quantifier) {
return propagate((Expr.Quantifier) expr, environment, context);
} else if (expr instanceof Expr.Constant) {
return propagate((Expr.Constant) expr, environment, context);
} else if (expr instanceof Expr.Cast) {
return propagate((Expr.Cast) expr, environment, context);
} else if (expr instanceof Expr.ConstantAccess) {
return propagate((Expr.ConstantAccess) expr, environment, context);
} else if (expr instanceof Expr.FieldAccess) {
return propagate((Expr.FieldAccess) expr, environment, context);
} else if (expr instanceof Expr.AbstractFunctionOrMethod) {
return propagate((Expr.AbstractFunctionOrMethod) expr, environment, context);
} else if (expr instanceof Expr.AbstractInvoke) {
return propagate((Expr.AbstractInvoke) expr, environment, context);
} else if (expr instanceof Expr.AbstractIndirectInvoke) {
return propagate((Expr.AbstractIndirectInvoke) expr, environment, context);
} else if (expr instanceof Expr.IndexOf) {
return propagate((Expr.IndexOf) expr, environment, context);
} else if (expr instanceof Expr.Lambda) {
return propagate((Expr.Lambda) expr, environment, context);
} else if (expr instanceof Expr.LocalVariable) {
return propagate((Expr.LocalVariable) expr, environment, context);
} else if (expr instanceof Expr.ArrayInitialiser) {
return propagate((Expr.ArrayInitialiser) expr, environment, context);
} else if (expr instanceof Expr.ArrayGenerator) {
return propagate((Expr.ArrayGenerator) expr, environment, context);
} else if (expr instanceof Expr.Dereference) {
return propagate((Expr.Dereference) expr, environment, context);
} else if (expr instanceof Expr.Record) {
return propagate((Expr.Record) expr, environment, context);
} else if (expr instanceof Expr.New) {
return propagate((Expr.New) expr, environment, context);
} else if (expr instanceof Expr.TypeVal) {
return propagate((Expr.TypeVal) expr, environment, context);
}
} catch (ResolveError e) {
syntaxError(errorMessage(RESOLUTION_ERROR, e.getMessage()), context, expr, e);
} catch (SyntaxError e) {
throw e;
} catch (Throwable e) {
internalFailure(e.getMessage(), context, expr, e);
return null; // dead code
}
internalFailure("unknown expression: " + expr.getClass().getName(), context, expr);
return null; // dead code
}
private Expr propagate(Expr.BinOp expr, Environment environment, Context context) throws IOException, ResolveError {
// TODO: split binop into arithmetic and conditional operators. This
// would avoid the following case analysis since conditional binary
// operators and arithmetic binary operators actually behave quite
// differently.
switch (expr.op) {
case AND:
case OR:
case XOR:
case EQ:
case NEQ:
case LT:
case LTEQ:
case GT:
case GTEQ:
case IS:
return propagateCondition(expr, true, environment, context).first();
}
Expr lhs = propagate(expr.lhs, environment, context);
Expr rhs = propagate(expr.rhs, environment, context);
expr.lhs = lhs;
expr.rhs = rhs;
Type lhsType = lhs.result();
Type rhsType = rhs.result();
Type srcType;
switch (expr.op) {
case IS:
case AND:
case OR:
case XOR:
return propagateCondition(expr, true, environment, context).first();
case BITWISEAND:
case BITWISEOR:
case BITWISEXOR:
checkIsSubtype(Type.T_BYTE, lhs, context, environment);
checkIsSubtype(Type.T_BYTE, rhs, context, environment);
srcType = Type.T_BYTE;
break;
case LEFTSHIFT:
case RIGHTSHIFT:
checkIsSubtype(Type.T_BYTE, lhs, context, environment);
checkIsSubtype(Type.T_INT, rhs, context, environment);
srcType = Type.T_BYTE;
break;
case RANGE:
checkIsSubtype(Type.T_INT, lhs, context, environment);
checkIsSubtype(Type.T_INT, rhs, context, environment);
srcType = Type.Array(Type.T_INT);
break;
case REM:
checkIsSubtype(Type.T_INT, lhs, context, environment);
checkIsSubtype(Type.T_INT, rhs, context, environment);
srcType = Type.T_INT;
break;
default:
// all other operations go through here
checkSuptypes(lhs, context, environment, Type.T_INT);
checkSuptypes(rhs, context, environment, Type.T_INT);
//
if (typeSystem.isSubtype(lhsType, rhsType) || typeSystem.isSubtype(rhsType, lhsType)) {
srcType = lhsType;
} else {
throw new SyntaxError(errorMessage(INCOMPARABLE_OPERANDS, lhsType, rhsType), file.getEntry(), expr);
}
}
expr.srcType = srcType;
return expr;
}
private Expr propagate(Expr.UnOp expr, Environment environment, Context context) throws IOException, ResolveError {
if (expr.op == Expr.UOp.NOT) {
// hand off to special method for conditions
return propagateCondition(expr, true, environment, context).first();
}
Expr src = propagate(expr.mhs, environment, context);
expr.mhs = src;
switch (expr.op) {
case NEG:
checkSuptypes(src, context, environment, Type.T_INT);
break;
case INVERT:
checkIsSubtype(Type.T_BYTE, src, context, environment);
break;
case ARRAYLENGTH: {
expr.type = expandAsEffectiveArray(expr.mhs, context);
return expr;
}
default:
internalFailure("unknown operator: " + expr.op.getClass().getName(), context, expr);
}
expr.type = src.result();
return expr;
}
private Expr propagate(Expr.Quantifier expr, Environment environment, Context context)
throws IOException, ResolveError {
ArrayList<Triple<String, Expr, Expr>> sources = expr.sources;
Environment local = environment.clone();
for (int i = 0; i != sources.size(); ++i) {
Triple<String, Expr, Expr> p = sources.get(i);
Expr start = propagate(p.second(), local, context);
Expr end = propagate(p.third(), local, context);
sources.set(i, new Triple<>(p.first(), start, end));
checkIsSubtype(Type.T_INT, start, context, environment);
local = local.declare(p.first(), Type.T_INT, Type.T_INT);
}
if (expr.condition != null) {
expr.condition = propagate(expr.condition, local, context);
}
expr.type = Type.T_BOOL;
local.free();
return expr;
}
private Expr propagate(Expr.Constant expr, Environment environment, Context context) {
return expr;
}
private Expr propagate(Expr.Cast c, Environment environment, Context context) throws IOException, ResolveError {
c.expr = propagate(c.expr, environment, context);
c.type = builder.toSemanticType(c.unresolvedType, context);
Type from = c.expr.result();
Type to = c.type;
if (!typeSystem.isExplicitCoerciveSubtype(to, from, environment.getLifetimeRelation())) {
syntaxError(errorMessage(SUBTYPE_ERROR, to, from), context, c);
}
return c;
}
private Expr propagate(Expr.AbstractFunctionOrMethod expr, Environment environment, Context context)
throws IOException, ResolveError {
if (expr instanceof Expr.FunctionOrMethod) {
return expr;
}
Triple<NameID, Type.FunctionOrMethod, List<String>> p;
if (expr.paramTypes != null) {
ArrayList<Type> paramTypes = new ArrayList<>();
for (SyntacticType t : expr.paramTypes) {
paramTypes.add(builder.toSemanticType(t, context));
}
// FIXME: clearly a bug here in the case of message reference
p = resolveAsFunctionOrMethod(expr.name, paramTypes, expr.lifetimeParameters, context, environment);
} else {
p = resolveAsFunctionOrMethod(expr.name, context, environment);
}
expr = new Expr.FunctionOrMethod(p.first(), expr.paramTypes, p.third, expr.attributes());
expr.type = p.second();
return expr;
}
private Expr propagate(Expr.Lambda expr, Environment environment, Context context)
throws IOException, ResolveError {
environment = environment.startLambda(expr.contextLifetimes, expr.lifetimeParameters);
List<WhileyFile.Parameter> expr_parameters = expr.parameters;
Type[] nomParameterTypes = new Type[expr_parameters.size()];
for (int i = 0; i != expr_parameters.size(); ++i) {
WhileyFile.Parameter p = expr_parameters.get(i);
Type n = builder.toSemanticType(p.type, context);
nomParameterTypes[i] = n;
// Now, update the environment to include those declared variables
String var = p.name();
if (environment.containsKey(var)) {
syntaxError(errorMessage(VARIABLE_ALREADY_DEFINED, var), context, p);
}
environment = environment.declare(var, n, n);
}
Type[] nomReturnTypes;
expr.body = propagate(expr.body, environment, context);
if (expr.body instanceof Expr.Multi) {
Expr.Multi m = (Expr.Multi) expr.body;
List<Type> returns = m.returns();
nomReturnTypes = new Type[returns.size()];
for (int i = 0; i != returns.size(); ++i) {
nomReturnTypes[i] = returns.get(i);
}
} else {
Type result = expr.body.result();
nomReturnTypes = new Type[] { result };
}
Type type;
if (Exprs.isPure(expr.body, context)) {
type = Type.Function(nomParameterTypes, nomReturnTypes);
} else {
type = Type.Method(expr.lifetimeParameters, expr.contextLifetimes, nomParameterTypes, nomReturnTypes);
}
expr.type = expandAsEffectiveFunctionOrMethod(type, expr, context);
return expr;
}
private Expr propagate(Expr.AbstractIndirectInvoke expr, Environment environment, Context context)
throws IOException, ResolveError {
// We can only invoke functions and methods
expr.src = propagate(expr.src, environment, context);
Type.FunctionOrMethod funType = expandAsEffectiveFunctionOrMethod(expr.src, context);
if (funType == null) {
syntaxError("function or method type expected", context, expr.src);
}
// Do we have matching argument count?
Type[] paramTypes = funType.params();
ArrayList<Expr> exprArgs = expr.arguments;
if (paramTypes.length != exprArgs.size()) {
syntaxError("insufficient arguments for function or method invocation", context, expr.src);
}
// resolve through arguments
ArrayList<Type> argTypes = new ArrayList<>();
for (int i = 0; i != exprArgs.size(); ++i) {
Expr arg = propagate(exprArgs.get(i), environment, context);
exprArgs.set(i, arg);
argTypes.add(arg.result());
}
if (funType instanceof Type.Function) {
// Check parameter types
for (int i = 0; i != exprArgs.size(); ++i) {
Type pt = paramTypes[i];
checkIsSubtype(pt, exprArgs.get(i), context, environment);
}
//
Expr.IndirectFunctionCall ifc = new Expr.IndirectFunctionCall(expr.src, exprArgs, expr.attributes());
ifc.functionType = (Type.Function) funType;
return ifc;
} else {
// Handle lifetime arguments
Type.Method methType = (Type.Method) funType;
List<String> lifetimeParameters = Arrays.asList(methType.lifetimeParams());
List<String> lifetimeArguments = expr.lifetimeArguments;
if (lifetimeArguments == null) {
// First consider the case where no lifetime arguments are
// specified.
if (lifetimeParameters.isEmpty()) {
// No lifetime arguments needed!
lifetimeArguments = Collections.emptyList();
} else {
// We have to guess proper lifetime arguments.
List<Type> rawArgTypes = stripType(argTypes);
List<ValidCandidate> validCandidates = new ArrayList<>();
guessLifetimeArguments(extractLifetimesFromArguments(rawArgTypes), lifetimeParameters,
Arrays.asList(funType.params()), rawArgTypes, null, // don't
// need
// a
// name
// id
funType, validCandidates, environment);
if (validCandidates.isEmpty()) {
// We were not able to guess lifetime arguments
syntaxError("no lifetime arguments specified and unable to infer them", context, expr.src);
}
if (validCandidates.size() == 1) {
// All right, we found proper lifetime arguments.
// Note that at this point we indeed have a method
// (not a function), because functions don't have
// lifetime parameters.
Expr.IndirectMethodCall imc = new Expr.IndirectMethodCall(expr.src, exprArgs,
validCandidates.get(0).lifetimeArguments, expr.attributes());
imc.methodType = (Type.Method) funType;
return imc;
}
// Arriving here means we have more than one possible
// solution. That is an ambiguity error, but we're nice and
// also print all solutions.
StringBuilder msg = new StringBuilder(
"no lifetime arguments specified and unable to infer a unique solution");
List<String> solutions = new ArrayList<>(validCandidates.size());
for (ValidCandidate vc : validCandidates) {
solutions.add(vc.lifetimeArguments.toString());
}
Collections.sort(solutions); // make error message
// deterministic!
for (String s : solutions) {
msg.append("\nfound solution: ");
msg.append(s);
}
syntaxError(msg.toString(), context, expr.src);
}
}
if (lifetimeParameters.size() != lifetimeArguments.size()) {
// Lifetime arguments specified, but number doesn't match
syntaxError("insufficient lifetime arguments for method invocation", context, expr.src);
}
// Check argument types with respect to specified lifetime arguments
Map<String, String> substitution = buildSubstitution(lifetimeParameters, lifetimeArguments);
for (int i = 0; i != exprArgs.size(); ++i) {
Type pt = paramTypes[i];
Expr arg = propagate(exprArgs.get(i), environment, context);
checkIsSubtype(applySubstitution(substitution, pt), arg, context, environment);
exprArgs.set(i, arg);
}
Expr.IndirectMethodCall imc = new Expr.IndirectMethodCall(expr.src, exprArgs, lifetimeArguments,
expr.attributes());
imc.methodType = methType;
return imc;
}
}
private Expr propagate(Expr.AbstractInvoke expr, Environment environment, Context context)
throws IOException, ResolveError {
// first, resolve through receiver and parameters.
Path.ID qualification = expr.qualification;
ArrayList<Expr> exprArgs = expr.arguments;
ArrayList<String> lifetimeArgs = expr.lifetimeArguments;
ArrayList<Type> paramTypes = new ArrayList<>();
for (int i = 0; i != exprArgs.size(); ++i) {
Expr arg = propagate(exprArgs.get(i), environment, context);
exprArgs.set(i, arg);
paramTypes.add(arg.result());
}
// second, determine the fully qualified name of this function based on
// the given function name and any supplied qualifications.
ArrayList<String> qualifications = new ArrayList<>();
if (expr.qualification != null) {
for (String n : expr.qualification) {
qualifications.add(n);
}
}
qualifications.add(expr.name);
NameID name = builder.resolveAsName(qualifications, context);
// third, lookup the appropriate function or method based on the name
// and given parameter types.
Triple<NameID, Type.FunctionOrMethod, List<String>> triple = resolveAsFunctionOrMethod(name, paramTypes,
lifetimeArgs, context, environment);
if (triple.second() instanceof Type.Function) {
Expr.FunctionCall r = new Expr.FunctionCall(name, qualification, exprArgs, expr.attributes());
r.functionType = (Type.Function) triple.second();
return r;
} else if (triple.second() instanceof Type.Method) {
Expr.MethodCall r = new Expr.MethodCall(name, qualification, exprArgs, triple.third(), expr.attributes());
r.methodType = (Type.Method) triple.second();
return r;
} else {
Expr.PropertyCall r = new Expr.PropertyCall(name, qualification, exprArgs, expr.attributes());
r.propertyType = (Type.Property) triple.second();
return r;
}
}
private Expr propagate(Expr.IndexOf expr, Environment environment, Context context)
throws IOException, ResolveError {
expr.src = propagate(expr.src, environment, context);
expr.index = propagate(expr.index, environment, context);
Type.EffectiveArray srcType = expandAsEffectiveArray(expr.src, context);
if (srcType == null) {
syntaxError(errorMessage(INVALID_ARRAY_EXPRESSION), context, expr.src);
} else {
expr.srcType = srcType;
}
checkIsSubtype(Type.T_INT, expr.index, context, environment);
return expr;
}
private Expr propagate(Expr.LocalVariable expr, Environment environment, Context context) throws IOException {
Type type = environment.getCurrentType(expr.var);
expr.type = type;
return expr;
}
private Expr propagate(Expr.ArrayInitialiser expr, Environment environment, Context context)
throws IOException, ResolveError {
Type element = Type.T_VOID;
ArrayList<Expr> exprs = expr.arguments;
for (int i = 0; i != exprs.size(); ++i) {
Expr e = propagate(exprs.get(i), environment, context);
Type t = e.result();
exprs.set(i, e);
element = Type.Union(t, element);
}
expr.type = (Type.Array) expandAsEffectiveArray(Type.Array(element), expr, context);
return expr;
}
private Expr propagate(Expr.ArrayGenerator expr, Environment environment, Context context)
throws ResolveError, IOException {
expr.element = propagate(expr.element, environment, context);
expr.count = propagate(expr.count, environment, context);
expr.type = (Type.Array) expandAsEffectiveArray(Type.Array(expr.element.result()), expr, context);
checkIsSubtype(Type.T_INT, expr.count, environment);
return expr;
}
private Expr propagate(Expr.Record expr, Environment environment, Context context)
throws IOException, ResolveError {
HashMap<String, Expr> exprFields = expr.fields;
ArrayList<Pair<Type, String>> fieldTypes = new ArrayList<>();
ArrayList<String> fields = new ArrayList<>(exprFields.keySet());
for (String field : fields) {
Expr e = propagate(exprFields.get(field), environment, context);
Type t = e.result();
exprFields.put(field, e);
fieldTypes.add(new Pair<>(t, field));
}
Type.Record anonType = (Type.Record) Type.Record(false, fieldTypes);
if(expr.name != null) {
// This indicates a named record initialiser. So, make sure we can
// resolve the name in question.
NameID name = builder.resolveAsName(expr.name, context);
// Expand name to a record type.
Type namedType = Type.Nominal(name);
Type.Record expandedType = (Type.Record) expandAsEffectiveRecord(namedType, expr, context);
String[] expandedFields = expandedType.getFieldNames();
// Begin checking field types
if(expandedFields.length != fields.size()) {
// FIXME: need to support open records
syntaxError(errorMessage(SUBTYPE_ERROR,expandedType,anonType),context,expr);
} else {
// Check that all fields match the appropriate types
for(int i=0;i!=expandedFields.length;++i) {
String fieldName = expandedFields[i];
Type anonFieldType = anonType.getField(fieldName);
Type namedFieldType = expandedType.getField(fieldName);
if(anonFieldType == null) {
syntaxError(errorMessage(RECORD_MISSING_FIELD, fieldName), context, expr);
}
checkIsSubtype(namedFieldType, anonFieldType, expr, environment);
}
}
expr.type = namedType;
} else {
expr.type = (Type.Record) expandAsEffectiveRecord(anonType, expr, context);
}
return expr;
}
private Expr propagate(Expr.FieldAccess ra, Environment environment, Context context)
throws IOException, ResolveError {
ra.src = propagate(ra.src, environment, context);
Type.EffectiveRecord recType = expandAsEffectiveRecord(ra.src, context);
if (recType.hasField(ra.name)) {
ra.srcType = recType;
return ra;
} else {
syntaxError(errorMessage(RECORD_MISSING_FIELD, ra.name), context, ra);
return deadCode(ra);
}
}
private Expr propagate(Expr.ConstantAccess expr, Environment environment, Context context) throws IOException {
// First, determine the fully qualified name of this function based on
// the given function name and any supplied qualifications.
ArrayList<String> qualifications = new ArrayList<>();
if (expr.qualification != null) {
for (String n : expr.qualification) {
qualifications.add(n);
}
}
qualifications.add(expr.name);
try {
NameID name = builder.resolveAsName(qualifications, context);
// Second, determine the value of the constant.
Pair<Constant, Type> ct = resolveAsConstant(name);
expr.value = ct.first();
expr.type = ct.second();
return expr;
} catch (ResolveError e) {
syntaxError(errorMessage(UNKNOWN_VARIABLE), context, expr);
return null;
}
}
private Expr propagate(Expr.Dereference expr, Environment environment, Context context)
throws IOException, ResolveError {
Expr src = propagate(expr.src, environment, context);
expr.src = src;
Type.Reference srcType = expandAsEffectiveReference(src, context);
if (srcType == null) {
syntaxError("invalid reference expression", context, src);
}
String lifetime = srcType.lifetime();
if (!environment.canDereferenceLifetime(lifetime)) {
syntaxError("lifetime '" + lifetime + "' cannot be dereferenced here", context, expr);
}
expr.srcType = srcType;
return expr;
}
private Expr propagate(Expr.New expr, Environment environment, Context context) throws IOException, ResolveError {
expr.expr = propagate(expr.expr, environment, context);
expr.type = expandAsEffectiveReference(Type.Reference(expr.lifetime, expr.expr.result()), expr, context);
return expr;
}
private Expr propagate(Expr.TypeVal expr, Environment environment, Context context)
throws IOException, ResolveError {
expr.type = builder.toSemanticType(expr.unresolvedType, context);
return expr;
}
private List<Pair<Expr, Type>> calculateTypesProduced(List<Expr> expressions) {
ArrayList<Pair<Expr, Type>> types = new ArrayList<>();
for (int i = 0; i != expressions.size(); ++i) {
Expr e = expressions.get(i);
if (e instanceof Expr.Multi) {
// The assigned expression actually has multiple returns,
// therefore extract them all.
Expr.Multi me = (Expr.Multi) e;
for (Type ret : me.returns()) {
types.add(new Pair<>(e, ret));
}
} else {
// The assigned rval is a simple expression which returns a
// single value
types.add(new Pair<>(e, e.result()));
}
}
return types;
}
// =========================================================================
// Resolve as Function or Method
// =========================================================================
/**
* Responsible for determining the true type of a method or function being
* invoked. To do this, it must find the function/method with the most
* precise type that matches the argument types.
*
* @param nid
* @param parameters
* @param lifetimeArgs
* --- lifetime arguments passed on method invocation, or null if
* none are passed and the compiler has to figure it out
* @return nameid, type, given/inferred lifetime arguments
* @throws IOException
*/
public Triple<NameID, Type.FunctionOrMethod, List<String>> resolveAsFunctionOrMethod(NameID nid,
List<Type> parameters, List<String> lifetimeArgs, Context context, Environment environment)
throws IOException, ResolveError {
// The set of candidate names and types for this function or method.
HashSet<Pair<NameID, Type.FunctionOrMethod>> candidates = new HashSet<>();
// First, add all valid candidates to the list without considering which
// is the most precise.
addCandidateFunctionsAndMethods(nid, parameters, candidates, context);
// Second, add to narrow down the list of candidates to a single choice.
// If this is impossible, then we have an ambiguity error.
return selectCandidateFunctionOrMethod(nid.name(), parameters, lifetimeArgs, candidates, context, environment);
}
/**
* Responsible for determining the true type of a method or function being
* invoked. In this case, no argument types are given. This means that any
* match is returned. However, if there are multiple matches, then an
* ambiguity error is reported.
*
* @param name
* --- function or method name whose type to determine.
* @param context
* --- context in which to resolve this name.
* @return
* @throws IOException
*/
public Triple<NameID, Type.FunctionOrMethod, List<String>> resolveAsFunctionOrMethod(String name, Context context,
Environment environment) throws IOException, ResolveError {
return resolveAsFunctionOrMethod(name, null, null, context, environment);
}
/**
* Responsible for determining the true type of a method or function being
* invoked. To do this, it must find the function/method with the most
* precise type that matches the argument types.
*
* @param name
* --- name of function or method whose type to determine.
* @param parameters
* --- required parameter types for the function or method.
* @param lifetimeArgs
* --- lifetime arguments passed on method invocation, or null if
* none are passed and the compiler has to figure it out
* @param context
* --- context in which to resolve this name.
* @return nameid, type, given/inferred lifetime arguments
* @throws IOException
*/
public Triple<NameID, Type.FunctionOrMethod, List<String>> resolveAsFunctionOrMethod(String name,
List<Type> parameters, List<String> lifetimeArgs, Context context, Environment environment)
throws IOException, ResolveError {
HashSet<Pair<NameID, Type.FunctionOrMethod>> candidates = new HashSet<>();
// first, try to find the matching message
for (WhileyFile.Import imp : context.imports()) {
String impName = imp.name;
if (impName == null || impName.equals(name) || impName.equals("*")) {
Trie filter = imp.filter;
if (impName == null) {
// import name is null, but it's possible that a module of
// the given name exists, in which case any matching names
// are automatically imported.
filter = filter.parent().append(name);
}
for (Path.ID mid : builder.imports(filter)) {
NameID nid = new NameID(mid, name);
addCandidateFunctionsAndMethods(nid, parameters, candidates, context);
}
}
}
return selectCandidateFunctionOrMethod(name, parameters, lifetimeArgs, candidates, context, environment);
}
/**
* @param f1_params
* @param f2_params
* @param environment
* @return whether f2_params are strict subtypes of f1_params
* @throws ResolveError
*/
private boolean paramStrictSubtypes(List<Type> f1_params, List<Type> f2_params, Environment environment)
throws ResolveError {
if (f1_params.size() == f2_params.size()) {
boolean allEquivalent = true;
for (int i = 0; i != f1_params.size(); ++i) {
Type f1_param = f1_params.get(i);
Type f2_param = f2_params.get(i);
if (!typeSystem.isSubtype(f1_param, f2_param, environment.getLifetimeRelation())) {
return false;
}
allEquivalent &= typeSystem.isSubtype(f2_param, f1_param, environment.getLifetimeRelation());
}
// This function returns true if the parameters are a strict
// subtype. Therefore, if they are all equivalent it must return false.
return !allEquivalent;
}
return false;
}
private String parameterString(List<Type> paramTypes) {
String paramStr = "(";
boolean firstTime = true;
if (paramTypes == null) {
paramStr += "...";
} else {
for (Type t : paramTypes) {
if (!firstTime) {
paramStr += ",";
}
firstTime = false;
paramStr += t;
}
}
return paramStr + ")";
}
private String foundCandidatesString(Collection<Pair<NameID, Type.FunctionOrMethod>> candidates) {
ArrayList<String> candidateStrings = new ArrayList<>();
for (Pair<NameID, Type.FunctionOrMethod> c : candidates) {
candidateStrings.add(c.first() + " : " + c.second());
}
Collections.sort(candidateStrings); // make error message deterministic!
StringBuilder msg = new StringBuilder();
for (String s : candidateStrings) {
msg.append("\n\tfound: ");
msg.append(s);
}
return msg.toString();
}
/**
* Extract all lifetime names from the types in the given list.
*
* We just walk through the type automata and collect the lifetime for each
* encountered reference.
*
* The result set will always contain the default lifetime "*".
*
* @param types
* the types to get the lifetimes from
* @return a set of all extracted lifetime names, without "*"
*/
private List<String> extractLifetimesFromArguments(Iterable<Type> types) {
Set<String> result = new HashSet<>();
for (Type t : types) {
extractLifetimes(t, result);
}
result.add("*");
return new ArrayList<>(result);
}
private void extractLifetimes(Type type, Set<String> lifetimes) {
if (type instanceof Type.Leaf) {
return;
} else if (type instanceof Type.Array) {
Type.Array t = (Type.Array) type;
extractLifetimes(t.element(), lifetimes);
} else if (type instanceof Type.Record) {
Type.Record t = (Type.Record) type;
for (String name : t.getFieldNames()) {
extractLifetimes(t.getField(name), lifetimes);
}
} else if (type instanceof Type.Reference) {
Type.Reference t = (Type.Reference) type;
extractLifetimes(t.element(), lifetimes);
lifetimes.add(t.lifetime());
} else if (type instanceof Type.Union) {
Type.Union t = (Type.Union) type;
extractLifetimes(t.bounds(), lifetimes);
} else if (type instanceof Type.Intersection) {
Type.Intersection t = (Type.Intersection) type;
extractLifetimes(t.bounds(), lifetimes);
} else if (type instanceof Type.Function) {
Type.Function t = (Type.Function) type;
extractLifetimes(t.params(), lifetimes);
extractLifetimes(t.returns(), lifetimes);
} else if (type instanceof Type.Method) {
Type.Method t = (Type.Method) type;
extractLifetimes(t.params(), lifetimes);
extractLifetimes(t.returns(), lifetimes);
ArrayUtils.addAll(t.contextLifetimes(), lifetimes);
} else {
Type.Negation t = (Type.Negation) type;
extractLifetimes(t.element(), lifetimes);
}
}
private void extractLifetimes(Type[] types, Set<String> lifetimes) {
for (int i = 0; i != types.length; ++i) {
extractLifetimes(types[i], lifetimes);
}
}
/**
* Container for a function/method candidate during method resolution.
*/
private static class ValidCandidate {
private final NameID id;
private final Type.FunctionOrMethod type;
// Either given (lifetimeArgs) or inferred
private final List<String> lifetimeArguments;
// Lifetime parameters substituted with (inferred) arguments
private final List<Type> parameterTypesSubstituted;
private ValidCandidate(NameID id, Type.FunctionOrMethod type, List<String> lifetimeArguments,
List<Type> parameterTypesSubstituted) {
this.id = id;
this.type = type;
this.lifetimeArguments = lifetimeArguments;
this.parameterTypesSubstituted = parameterTypesSubstituted;
}
}
/**
* Highly optimized method to validate a function/method candidate.
*
* @param candidateId
* @param candidateType
* @param candidateParameterTypes
* @param targetParameterTypes
* @param lifetimeParameters
* @param lifetimeArguments
* @param environment
* @return
* @throws ResolveError
*/
private ValidCandidate validateCandidate(NameID candidateId, Type.FunctionOrMethod candidateType,
List<Type> candidateParameterTypes, List<Type> targetParameterTypes, List<String> lifetimeParameters,
List<String> lifetimeArguments, Environment environment) throws ResolveError {
if (!lifetimeParameters.isEmpty()) {
// Here we *might* need a substitution
Map<String, String> substitution = buildSubstitution(lifetimeParameters, lifetimeArguments);
if (!substitution.isEmpty()) {
// OK, substitution is necessary.
Iterator<Type> itC = candidateParameterTypes.iterator();
Iterator<Type> itT = targetParameterTypes.iterator();
List<Type> parameterTypesSubstituted = new ArrayList<>(candidateParameterTypes.size());
while (itC.hasNext()) {
Type c = itC.next();
Type t = itT.next();
c = applySubstitution(substitution, c);
if (!typeSystem.isSubtype(c, t, environment.getLifetimeRelation())) {
return null;
}
parameterTypesSubstituted.add(c);
}
return new ValidCandidate(candidateId, candidateType, lifetimeArguments, parameterTypesSubstituted);
}
}
// No substitution necessary, just do the check
Iterator<Type> itC = candidateParameterTypes.iterator();
Iterator<Type> itT = targetParameterTypes.iterator();
while (itC.hasNext()) {
Type c = itC.next();
Type t = itT.next();
if (!typeSystem.isSubtype(c, t, environment.getLifetimeRelation())) {
return null;
}
}
return new ValidCandidate(candidateId, candidateType, Collections.<String> emptyList(),
candidateParameterTypes);
}
private Triple<NameID, Type.FunctionOrMethod, List<String>> selectCandidateFunctionOrMethod(String name,
List<Type> parameters, List<String> lifetimeArgs,
Collection<Pair<NameID, Type.FunctionOrMethod>> candidates, Context context, Environment environment)
throws IOException, ResolveError {
// We cannot do anything here without candidates
if (candidates.isEmpty()) {
throw new ResolveError("no match for " + name + parameterString(parameters));
}
// If we don't match parameters, then we don't need to bother about
// lifetimes. Handle it separately to avoid null checks in further
// logic.
if (parameters == null) {
if (candidates.size() == 1) {
Pair<NameID, Type.FunctionOrMethod> p = candidates.iterator().next();
return new Triple<>(p.first(), p.second(), null);
}
// More than one candidate and all will match. Clearly ambiguous!
throw new ResolveError(
name + parameterString(parameters) + " is ambiguous" + foundCandidatesString(candidates));
}
// We chose a method based only on the parameter types, as return
// type(s) are unknown.
List<Type> targetParameterTypes = stripType(parameters);
// In case we don't get lifetime arguments, we have to pick a possible
// substitution by guessing. To do so, we need all lifetime names that
// occur in the passed argument types. We will cache it here once we
// compute it (only compute it if needed.
List<String> lifetimesUsedInArguments = null;
// Check each candidate to see if it is valid.
List<ValidCandidate> validCandidates = new LinkedList<>();
for (Pair<NameID, Type.FunctionOrMethod> p : candidates) {
Type.FunctionOrMethod candidateType = p.second();
List<Type> candidateParameterTypes = Arrays.asList(candidateType.params());
// We need a matching parameter count
if (candidateParameterTypes.size() != targetParameterTypes.size()) {
continue;
}
// If we got lifetime arguments: Lifetime parameter count must match
List<String> candidateLifetimeParams = getLifetimeParameters(candidateType);
if (lifetimeArgs != null && candidateLifetimeParams.size() != lifetimeArgs.size()) {
continue;
}
if (candidateLifetimeParams.size() == 0) {
// We don't need lifetime arguments, so just provide an empty
// list.
ValidCandidate vc = validateCandidate(p.first(), candidateType, candidateParameterTypes,
targetParameterTypes, candidateLifetimeParams, Collections.<String> emptyList(), environment);
if (vc != null) {
validCandidates.add(vc);
}
} else if (lifetimeArgs != null) {
// We got some lifetime arguments. Just check it with them.
ValidCandidate vc = validateCandidate(p.first(), candidateType, candidateParameterTypes,
targetParameterTypes, candidateLifetimeParams, lifetimeArgs, environment);
if (vc != null) {
validCandidates.add(vc);
}
} else {
// Here it is a bit tricky:
// We need to "guess" suitable lifetime arguments.
// Make sure we know all lifetime names from our arguments, and
// cache the result for the next candidate.
if (lifetimesUsedInArguments == null) {
lifetimesUsedInArguments = extractLifetimesFromArguments(targetParameterTypes);
}
// Guess the lifetime arguments.
guessLifetimeArguments(lifetimesUsedInArguments, candidateLifetimeParams, candidateParameterTypes,
targetParameterTypes, p.first(), candidateType, validCandidates, environment);
}
}
// See if we have valid candidates
if (validCandidates.isEmpty()) {
// No valid candidates
throw new ResolveError(
"no match for " + name + parameterString(parameters) + foundCandidatesString(candidates));
}
// More than one candidate
if (validCandidates.size() != 1) {
// Idea: We iterate through the list and delete a valid candidate,
// if there is another one that is a strict subtype.
// The outer iterator is used to actually modify the list by
// removing candidates.
ListIterator<ValidCandidate> it = validCandidates.listIterator();
// we know that the list is not empty, so do-while is perfectly fine
// here
do {
ValidCandidate c1 = it.next();
// Let the inner iterator start at the next entry. Note that the
// list initially had > 1 elements and the outer do-while also
// checks that there is one more element left, so we can again
// use do-while here.
for (ValidCandidate c2 : validCandidates) {
if (c1 != c2 && paramStrictSubtypes(c1.parameterTypesSubstituted, c2.parameterTypesSubstituted,
environment)) {
it.remove();
break;
}
}
} while (it.hasNext());
}
if (validCandidates.size() == 1) {
// now check protection modifier
ValidCandidate winner = validCandidates.get(0);
NameID winnerId = winner.id;
Type.FunctionOrMethod winnerType = winner.type;
WhileyFile wf = builder.getSourceFile(winnerId.module());
if (wf != null) {
if (wf != context.file()) {
for (WhileyFile.FunctionOrMethodOrProperty d : wf.declarations(WhileyFile.FunctionOrMethodOrProperty.class,
winnerId.name())) {
if (d.parameters.equals(winnerType.params())) {
if (!d.hasModifier(Modifier.PUBLIC)) {
String msg = winnerId.module() + "." + name + parameterString(parameters)
+ " is not visible";
throw new ResolveError(msg);
}
}
}
}
} else {
WyilFile m = builder.getModule(winnerId.module());
WyilFile.FunctionOrMethodOrProperty d = m.functionOrMethodOrProperty(winnerId.name(), winnerType);
if (!d.hasModifier(Modifier.PUBLIC)) {
String msg = winnerId.module() + "." + name + parameterString(parameters) + " is not visible";
throw new ResolveError(msg);
}
}
return new Triple<>(winnerId, winnerType,
winner.lifetimeArguments);
}
// this is an ambiguous error
StringBuilder msg = new StringBuilder(name + parameterString(parameters) + " is ambiguous");
ArrayList<String> candidateStrings = new ArrayList<>();
for (ValidCandidate c : validCandidates) {
String s = c.id + " : " + c.type;
if (!c.lifetimeArguments.isEmpty()) {
s += " instantiated with <";
boolean first = true;
for (String lifetime : c.lifetimeArguments) {
if (!first) {
s += ", ";
} else {
first = false;
}
s += lifetime;
}
s += ">";
}
candidateStrings.add(s);
}
Collections.sort(candidateStrings); // make error message deterministic!
for (String s : candidateStrings) {
msg.append("\n\tfound: ");
msg.append(s);
}
throw new ResolveError(msg.toString());
}
private static List<String> getLifetimeParameters(Type.FunctionOrMethod fm) {
if (fm instanceof Type.Method) {
Type.Method mt = (Type.Method) fm;
return Arrays.asList(mt.lifetimeParams());
} else {
return Collections.EMPTY_LIST;
}
}
/**
* Guess lifetime arguments for a method call.
*
* @param lifetimesUsedInArguments
* possible choices for lifetimes to be used as argument
* @param candidateLifetimeParams
* lifetime parameters to be assigned with an argument
* @param candidateParameterTypes
* parameter types of the actual declared method
* @param targetParameterTypes
* parameter types as needed (extracted from caller arguments)
* @param candidateName
* @param candidateType
* @param validCandidates
* the set where we can put valid substitutions
* @param environment
* @throws ResolveError
*/
private void guessLifetimeArguments(List<String> lifetimesUsedInArguments, List<String> candidateLifetimeParams,
List<Type> candidateParameterTypes, List<Type> targetParameterTypes, NameID candidateName,
Type.FunctionOrMethod candidateType, List<ValidCandidate> validCandidates, Environment environment)
throws ResolveError {
// Assume we have "exp" lifetime parameters to be filled and
// "base" choices for each one.
// That makes base^exp possibilities!
int base = lifetimesUsedInArguments.size();
int exp = candidateLifetimeParams.size();
long count = (long) Math.pow(base, exp);
for (long guessNumber = 0; guessNumber < count; guessNumber++) {
// Here we generate a guessed list of lifetime arguments.
// Basically it is the algorithm to transform guessNumber to
// base size(lifetimesUsedInArguments).
List<String> guessedLifetimeArgs = new ArrayList<>(candidateLifetimeParams.size());
for (int i = 0; i < exp; i++) {
int guessed = (int) ((guessNumber / (long) Math.pow(base, i)) % base);
guessedLifetimeArgs.add(lifetimesUsedInArguments.get(guessed));
}
// Now we can check the candidate with our guess
ValidCandidate vc = validateCandidate(candidateName, candidateType, candidateParameterTypes,
targetParameterTypes, candidateLifetimeParams, guessedLifetimeArgs, environment);
if (vc != null) {
validCandidates.add(vc);
}
}
}
/**
* Add all "candidate" functions or methods matching a fully qualified name.
* Candidates are those which the same name and matching number of
* arguments. They must also be visible to the given context. The context is
* important because some functions and methods will not be visible from all
* contexts. For example, a private function is only visible from within the
* same source file. Furthermore, the context affects what exactly will be
* seen externally. For example, consider a function "f(T x)=>int" where
* type "T" is declared as protected within the same file. The external type
* of "f" will be "(T)=>int"; however, the internal type will be e.g.
* "(int)=>int" if T is defined as int.
*
* @param nid
* --- Fully qualified name of function being matched
* @param parameters
* --- The list of parameter types, although this is only used to
* determine the number of parameters
* @param candidates
* --- The list into which all identified candidates will be
* placed (i.e. this is an output parameter)
* @param context
* --- The context in which we are looking for the given method.
* @throws IOException
*/
private void addCandidateFunctionsAndMethods(NameID nid, List<?> parameters,
Collection<Pair<NameID, Type.FunctionOrMethod>> candidates, Context context)
throws IOException, ResolveError {
Path.ID mid = nid.module();
int nparams = parameters != null ? parameters.size() : -1;
WhileyFile wf = builder.getSourceFile(mid);
if (wf != null) {
for (WhileyFile.FunctionOrMethodOrProperty f : wf.declarations(WhileyFile.FunctionOrMethodOrProperty.class, nid.name())) {
if (nparams == -1 || f.parameters.size() == nparams) {
Type.FunctionOrMethod ft = (Type.FunctionOrMethod) builder.toSemanticType(f.unresolvedType(), f);
candidates.add(new Pair<>(nid, ft));
}
}
} else {
WyilFile m = builder.getModule(mid);
for (WyilFile.FunctionOrMethod mm : m.functionOrMethods()) {
if ((mm.isFunction() || mm.isMethod()) && mm.name().equals(nid.name())
&& (nparams == -1 || mm.type().params().length == nparams)) {
Type.FunctionOrMethod t = mm.type();
candidates.add(new Pair<>(nid, t));
}
}
}
}
private static List<Type> stripType(List<Type> types) {
ArrayList<Type> r = new ArrayList<>();
for (Type t : types) {
r.add(t);
}
return r;
}
/**
* Apply a lifetime substitution: Substitute all parameters in original by
* their arguments.
*
* @param lifetimeParameters
* @param lifetimeArguments
* @param original
* @return
*/
public static Type applySubstitution(List<String> lifetimeParameters, List<String> lifetimeArguments,
Type original) {
if (lifetimeParameters.size() != lifetimeArguments.size()) {
throw new IllegalArgumentException(
"lifetime parameter/argument size mismatch!" + lifetimeParameters + " vs. " + lifetimeArguments);
}
Map<String, String> substitution = buildSubstitution(lifetimeParameters, lifetimeArguments);
return applySubstitution(substitution, original);
}
private static Type applySubstitution(Map<String, String> substitution, Type type) {
if (substitution.isEmpty()) {
return type;
} else {
return substitute(substitution, type);
}
}
private static Type substitute(Map<String, String> substitution, Type type) {
if (type instanceof Type.Primitive) {
// A primitive cannot contain any lifetime arguments, hence can be
// returned as itself.
return type;
} else if (type instanceof Type.Nominal) {
// At the moment, nominals also cannot contain unbound lifetime
// arguments. In the future this will change as nominals will
// support explicit lifetime arguments.
return type;
} else if (type instanceof Type.Array) {
Type.Array t = (Type.Array) type;
Type element = substitute(substitution, t.element());
return Type.Array(element);
} else if (type instanceof Type.Reference) {
Type.Reference t = (Type.Reference) type;
Type element = substitute(substitution, t.element());
// Apply the substitution!
String lifetime = substitution.get(t.lifetime());
if (lifetime == null) {
// This lifetime variable is not being substituted for whatever
// reason.
lifetime = t.lifetime();
}
return Type.Reference(lifetime, element);
} else if (type instanceof Type.Record) {
Type.Record t = (Type.Record) type;
String[] fieldNames = t.getFieldNames();
Pair<Type, String>[] fields = new Pair[fieldNames.length];
for (int i = 0; i != fieldNames.length; ++i) {
String fieldName = fieldNames[i];
// FIXME: would be more efficient to access by field index
Type element = substitute(substitution, t.getField(fieldName));
fields[i] = new Pair<>(element, fieldName);
}
return Type.Record(t.isOpen(), fields);
} else if (type instanceof Type.Function) {
Type.Function t = (Type.Function) type;
Type[] parameters = substitute(substitution, t.params());
Type[] returns = substitute(substitution, t.returns());
return Type.Function(parameters, returns);
} else if (type instanceof Type.Method) {
Type.Method t = (Type.Method) type;
String[] contextLifetimes = t.contextLifetimes();
// Apply substitution to all context lifetimes.
for (int i = 0; i != contextLifetimes.length; ++i) {
String lifetime = substitution.get(contextLifetimes[i]);
if (lifetime != null) {
// Substitution resulted in a change
if (contextLifetimes == t.contextLifetimes()) {
contextLifetimes = Arrays.copyOf(contextLifetimes, contextLifetimes.length);
}
contextLifetimes[i] = lifetime;
}
}
// Create a clone of the substitution so we can cut out any lifetime
// parameters declared by this method. This is necessary to avoid
// any potential variable captures.
HashMap<String, String> nSubstitution = new HashMap<>(substitution);
// Remove lifetime parameters from keys in the substitution map
String[] lifetimeParameters = t.lifetimeParams();
for (int i = 0; i != lifetimeParameters.length; ++i) {
String declaredLifetime = lifetimeParameters[i];
nSubstitution.remove(declaredLifetime);
if (substitution.containsValue(declaredLifetime)) {
// This is a big problem. Basically, because we will
// potentially have an unexpected variable capture if we
// perform a substitution which yields this value.
// FIXME: Do something more sensible here. What we need to
// do is replace the given declared lifetime with a fresh
// name which doesn't clash with anything else.
throw new RuntimeException("Need support for lifetime variable capture");
}
}
// Perform the substitution
Type[] parameters = substitute(nSubstitution, t.params());
Type[] returns = substitute(nSubstitution, t.returns());
return Type.Method(lifetimeParameters, contextLifetimes, parameters, returns);
} else if (type instanceof Type.Union) {
Type.Union t = (Type.Union) type;
Type[] bounds = substitute(substitution, t.bounds());
return Type.Union(bounds);
} else if (type instanceof Type.Intersection) {
Type.Intersection t = (Type.Intersection) type;
Type[] bounds = substitute(substitution, t.bounds());
return Type.Intersection(bounds);
} else {
Type.Negation t = (Type.Negation) type;
Type element = substitute(substitution, t.element());
return Type.Negation(element);
}
}
private static Type[] substitute(Map<String, String> substitution, Type[] types) {
Type[] nTypes = new Type[types.length];
for (int i = 0; i != types.length; ++i) {
nTypes[i] = substitute(substitution, types[i]);
}
return nTypes;
}
private static Map<String, String> buildSubstitution(List<String> lifetimeParameters,
List<String> lifetimeArguments) {
Map<String, String> substitution = new HashMap<>();
Iterator<String> itP = lifetimeParameters.iterator();
Iterator<String> itA = lifetimeArguments.iterator();
while (itP.hasNext()) {
String param = itP.next();
String arg = itA.next();
if (!arg.equals(param)) {
substitution.put(param, arg);
}
}
return substitution;
}
// =========================================================================
// ResolveAsType
// =========================================================================
public Type.Function resolveAsType(SyntacticType.Function t, Context context) throws IOException {
return (Type.Function) resolveAsType((SyntacticType.FunctionOrMethod) t, context);
}
public Type.Method resolveAsType(SyntacticType.Method t, Context context) throws IOException {
return (Type.Method) resolveAsType((SyntacticType.FunctionOrMethod) t, context);
}
public Type.Property resolveAsType(SyntacticType.Property t, Context context) throws IOException {
return (Type.Property) resolveAsType((SyntacticType.FunctionOrMethod) t, context);
}
public Type.FunctionOrMethod resolveAsType(SyntacticType.FunctionOrMethod t, Context context) throws IOException {
try {
// We need to sanity check the parameter types we have here, since
// occasionally we can end up with something other than a function
// type.
// This may seem surprising, but it can happen when one of the types
// involved is contractive (normally by accident).
for (SyntacticType param : t.paramTypes) {
Type nominal = builder.toSemanticType(param, context);
if (typeSystem.isSubtype(Type.T_VOID, nominal)) {
throw new SyntaxError("empty type encountered", file.getEntry(), param);
}
}
for (SyntacticType ret : t.returnTypes) {
Type nominal = builder.toSemanticType(ret, context);
if (typeSystem.isSubtype(Type.T_VOID, nominal)) {
throw new SyntaxError("empty type encountered", file.getEntry(), ret);
}
}
return (Type.FunctionOrMethod) builder.toSemanticType(t, context);
} catch (ResolveError e) {
throw new SyntaxError(errorMessage(RESOLUTION_ERROR, e.getMessage()), file.getEntry(), t, e);
}
}
// =========================================================================
// ResolveAsConstant
// =========================================================================
/**
* <p>
* Resolve a given name as a constant value. This is a global problem, since
* a constant declaration in one source file may refer to constants declared
* in other compilation units. This function will actually evaluate constant
* expressions (e.g. "1+2") to produce actual constant vales.
* </p>
*
* <p>
* Constant declarations form a global graph spanning multiple compilation
* units. In resolving a given constant, this function must traverse those
* portions of the graph which make up the constant. Constants are not
* permitted to be declared recursively (i.e. in terms of themselves) and
* this function will report an error is such a recursive cycle is detected
* in the constant graph.
* </p>
*
* @param nid
* Fully qualified name identifier of constant to resolve
* @return Constant value representing named constant
* @throws IOException
*/
public Pair<Constant, Type> resolveAsConstant(NameID nid) throws IOException, ResolveError {
return resolveAsConstant(nid, new HashSet<NameID>());
}
/**
* <p>
* Resolve a given <i>constant expression</i> as a constant value. A
* constant expression is one which refers only to known and visible
* constant values, rather than e.g. local variables. Constant expressions
* may still use operators (e.g. "1+2", or "1+c" where c is a declared
* constant).
* </p>
*
* <p>
* Constant expressions used in a few places in Whiley. In particular, the
* cases of a <code>switch</code> statement must be defined using constant
* expressions.
* </p>
*
* @param e
* @param context
* @return
*/
public Pair<Constant, Type> resolveAsConstant(Expr e, Context context) {
e = propagate(e, new Environment(), context);
return resolveAsConstant(e, context, new HashSet<NameID>());
}
/**
* Responsible for turning a named constant expression into a value. This is
* done by traversing the constant's expression and recursively expanding
* any named constants it contains. Simplification of constants is also
* performed where possible.
*
* @param key
* --- name of constant we are expanding.
* @param exprs
* --- mapping of all names to their( declared) expressions
* @param visited
* --- set of all constants seen during this traversal (used to
* detect cycles).
* @return
* @throws IOException
*/
private Pair<Constant, Type> resolveAsConstant(NameID key, HashSet<NameID> visited)
throws IOException, ResolveError {
Pair<Constant, Type> result = constantCache.get(key);
if (result != null) {
return result;
} else if (visited.contains(key)) {
throw new ResolveError("cyclic constant definition encountered (" + key + " -> " + key + ")");
} else {
visited.add(key);
}
WhileyFile wf = builder.getSourceFile(key.module());
if (wf != null) {
WhileyFile.Declaration decl = wf.declaration(key.name());
if (decl instanceof WhileyFile.Constant) {
WhileyFile.Constant cd = (WhileyFile.Constant) decl;
if (cd.resolvedValue == null) {
cd.constant = propagate(cd.constant, new Environment(), cd);
cd.resolvedValue = resolveAsConstant(cd.constant, cd, visited).first();
}
result = new Pair<>(cd.resolvedValue, cd.constant.result());
} else {
throw new ResolveError("unable to find constant " + key);
}
} else {
WyilFile module = builder.getModule(key.module());
WyilFile.Constant cd = module.constant(key.name());
if (cd != null) {
Constant c = cd.constant();
result = new Pair<>(c, c.type());
} else {
throw new ResolveError("unable to find constant " + key);
}
}
constantCache.put(key, result);
return result;
}
/**
* The following is a helper method for resolveAsConstant. It takes a given
* expression (rather than the name of a constant) and expands to a value
* (where possible). If the expression contains, for example, method or
* function declarations then this will certainly fail (producing a syntax
* error).
*
* @param key
* --- name of constant we are expanding.
* @param context
* --- context in which to resolve this constant.
* @param visited
* --- set of all constants seen during this traversal (used to
* detect cycles).
*/
private Pair<Constant, Type> resolveAsConstant(Expr expr, Context context, HashSet<NameID> visited) {
try {
if (expr instanceof Expr.Constant) {
Expr.Constant c = (Expr.Constant) expr;
return new Pair<>(c.value, c.result());
} else if (expr instanceof Expr.ConstantAccess) {
Expr.ConstantAccess c = (Expr.ConstantAccess) expr;
ArrayList<String> qualifications = new ArrayList<>();
if (c.qualification != null) {
for (String n : c.qualification) {
qualifications.add(n);
}
}
qualifications.add(c.name);
try {
NameID nid = builder.resolveAsName(qualifications, context);
return resolveAsConstant(nid, visited);
} catch (ResolveError e) {
syntaxError(errorMessage(UNKNOWN_VARIABLE), context, expr);
return null;
}
} else if (expr instanceof Expr.BinOp) {
Expr.BinOp bop = (Expr.BinOp) expr;
Pair<Constant, Type> lhs = resolveAsConstant(bop.lhs, context, visited);
Pair<Constant, Type> rhs = resolveAsConstant(bop.rhs, context, visited);
return new Pair<>(evaluate(bop, lhs.first(), rhs.first(), context), lhs.second());
} else if (expr instanceof Expr.UnOp) {
Expr.UnOp uop = (Expr.UnOp) expr;
Pair<Constant, Type> lhs = resolveAsConstant(uop.mhs, context, visited);
return new Pair<>(evaluate(uop, lhs.first(), context), lhs.second());
} else if (expr instanceof Expr.ArrayInitialiser) {
Expr.ArrayInitialiser nop = (Expr.ArrayInitialiser) expr;
ArrayList<Constant> values = new ArrayList<>();
Type element = Type.T_VOID;
for (Expr arg : nop.arguments) {
Pair<Constant, Type> e = resolveAsConstant(arg, context, visited);
values.add(e.first());
element = Type.Union(element, e.second());
}
return new Pair<>(new Constant.Array(values), Type.Array(element));
} else if (expr instanceof Expr.ArrayGenerator) {
Expr.ArrayGenerator lg = (Expr.ArrayGenerator) expr;
Pair<Constant, Type> element = resolveAsConstant(lg.element, context, visited);
Pair<Constant, Type> count = resolveAsConstant(lg.count, context, visited);
Constant.Array l = evaluate(lg, element.first(), count.first(), context);
return new Pair<>(l, Type.Array(element.second()));
} else if (expr instanceof Expr.Record) {
Expr.Record rg = (Expr.Record) expr;
HashMap<String, Constant> values = new HashMap<>();
ArrayList<Pair<Type, String>> types = new ArrayList<>();
for (Map.Entry<String, Expr> e : rg.fields.entrySet()) {
Pair<Constant, Type> v = resolveAsConstant(e.getValue(), context, visited);
if (v == null) {
return null;
}
values.put(e.getKey(), v.first());
types.add(new Pair<>(v.second(), e.getKey()));
}
return new Pair<>(new Constant.Record(values), Type.Record(false, types));
} else if (expr instanceof Expr.FunctionOrMethod) {
// TODO: add support for proper lambdas
Expr.FunctionOrMethod f = (Expr.FunctionOrMethod) expr;
return new Pair<>(new Constant.FunctionOrMethod(f.nid, f.type), f.type);
}
} catch (SyntaxError.InternalFailure e) {
throw e;
} catch (Throwable e) {
internalFailure(e.getMessage(), context, expr, e);
}
internalFailure("unknown constant expression: " + expr.getClass().getName(), context, expr);
return deadCode(expr);
}
// =========================================================================
// Constant Evaluation
// =========================================================================
/**
* Evaluate a given unary operator on a given input value.
*
* @param operator
* Unary operator to evaluate
* @param operand
* Operand to apply operator on
* @param context
* Context in which to apply operator (useful for error
* reporting)
* @return
*/
private Constant evaluate(Expr.UnOp operator, Constant operand, Context context) {
switch (operator.op) {
case NOT:
if (operand instanceof Constant.Bool) {
Constant.Bool b = (Constant.Bool) operand;
return Constant.Bool(!b.value());
}
syntaxError(errorMessage(INVALID_BOOLEAN_EXPRESSION), context, operator);
break;
case NEG:
if (operand instanceof Constant.Integer) {
Constant.Integer b = (Constant.Integer) operand;
return new Constant.Integer(b.value().negate());
}
syntaxError(errorMessage(INVALID_NUMERIC_EXPRESSION), context, operator);
break;
case INVERT:
if (operand instanceof Constant.Byte) {
Constant.Byte b = (Constant.Byte) operand;
return new Constant.Byte((byte) ~b.value());
}
break;
}
syntaxError(errorMessage(INVALID_UNARY_EXPRESSION), context, operator);
return null;
}
private Constant evaluate(Expr.BinOp bop, Constant v1, Constant v2, Context context) throws ResolveError {
Type v1_type = v1.type();
Type v2_type = v2.type();
if (typeSystem.isSubtype(Type.T_BOOL, v1_type) && typeSystem.isSubtype(Type.T_BOOL, v2_type)) {
return evaluateBoolean(bop, (Constant.Bool) v1, (Constant.Bool) v2, context);
} else if (typeSystem.isSubtype(Type.T_INT, v1_type) && typeSystem.isSubtype(Type.T_INT, v2_type)) {
return evaluate(bop, (Constant.Integer) v1, (Constant.Integer) v2, context);
}
syntaxError(errorMessage(INVALID_BINARY_EXPRESSION), context, bop);
return null;
}
private Constant evaluateBoolean(Expr.BinOp bop, Constant.Bool v1, Constant.Bool v2, Context context) {
switch (bop.op) {
case AND:
return Constant.Bool(v1.value() & v2.value());
case OR:
return Constant.Bool(v1.value() | v2.value());
case XOR:
return Constant.Bool(v1.value() ^ v2.value());
}
syntaxError(errorMessage(INVALID_BOOLEAN_EXPRESSION), context, bop);
return null;
}
private Constant evaluate(Expr.BinOp bop, Constant.Integer v1, Constant.Integer v2, Context context) {
switch (bop.op) {
case ADD:
return new Constant.Integer(v1.value().add(v2.value()));
case SUB:
return new Constant.Integer(v1.value().subtract(v2.value()));
case MUL:
return new Constant.Integer(v1.value().multiply(v2.value()));
case DIV:
return new Constant.Integer(v1.value().divide(v2.value()));
case REM:
return new Constant.Integer(v1.value().remainder(v2.value()));
}
syntaxError(errorMessage(INVALID_NUMERIC_EXPRESSION), context, bop);
return null;
}
private Constant.Array evaluate(Expr.ArrayGenerator bop, Constant element, Constant count, Context context) {
if (count instanceof Constant.Integer) {
Constant.Integer c = (Constant.Integer) count;
ArrayList<Constant> items = new ArrayList<>();
for (int i = 0; i != c.value().intValue(); ++i) {
items.add(element);
}
return new Constant.Array(items);
}
syntaxError(errorMessage(INVALID_ARRAY_EXPRESSION), context, bop);
return null;
}
// =========================================================================
// expandAsType
// =========================================================================
public Type.EffectiveArray expandAsEffectiveArray(Expr src, Context context) throws IOException, ResolveError {
return expandAsEffectiveArray(src.result(), src, context);
}
public Type.EffectiveArray expandAsEffectiveArray(Type type, SyntacticElement element, Context context)
throws IOException, ResolveError {
Type.EffectiveArray arrType = typeSystem.expandAsEffectiveArray(type);
if (arrType == null) {
syntaxError(errorMessage(INVALID_ARRAY_EXPRESSION), context, element);
}
return arrType;
}
public Type.EffectiveRecord expandAsEffectiveRecord(Expr src, Context context) throws IOException, ResolveError {
return expandAsEffectiveRecord(src.result(), src, context);
}
public Type.EffectiveRecord expandAsEffectiveRecord(Type type, SyntacticElement element, Context context)
throws IOException, ResolveError {
Type.EffectiveRecord recType = typeSystem.expandAsEffectiveRecord(type);
if (recType == null) {
syntaxError(errorMessage(RECORD_TYPE_REQUIRED, type), context, element);
}
return recType;
}
public Type.Reference expandAsEffectiveReference(Expr src, Context context) throws IOException, ResolveError {
return expandAsEffectiveReference(src.result(), src, context);
}
public Type.Reference expandAsEffectiveReference(Type type, SyntacticElement element, Context context)
throws IOException, ResolveError {
Type.Reference refType = typeSystem.expandAsReference(type);
if (refType == null) {
syntaxError(errorMessage(REFERENCE_TYPE_REQUIRED, type), context, element);
}
return refType;
}
public Type.FunctionOrMethod expandAsEffectiveFunctionOrMethod(Expr src, Context context)
throws IOException, ResolveError {
return expandAsEffectiveFunctionOrMethod(src.result(), src, context);
}
public Type.FunctionOrMethod expandAsEffectiveFunctionOrMethod(Type type, SyntacticElement element, Context context)
throws IOException, ResolveError {
Type.FunctionOrMethod funType = typeSystem.expandAsFunctionOrMethod(type);
if (funType == null) {
syntaxError(errorMessage(FUNCTION_OR_METHOD_TYPE_REQUIRED, type), context, element);
}
return funType;
}
private Environment addDeclaredParameters(List<WhileyFile.Parameter> parameters, Environment environment,
WhileyFile.Context d) throws IOException {
for (WhileyFile.Parameter p : parameters) {
try {
environment = environment.declare(p.name, builder.toSemanticType(p.type, d),
builder.toSemanticType(p.type, d));
} catch (ResolveError e) {
throw new SyntaxError(errorMessage(RESOLUTION_ERROR, e.getMessage()), file.getEntry(), p.type, e);
}
}
return environment;
}
private Environment addDeclaredParameter(WhileyFile.Parameter parameter, Environment environment,
WhileyFile.Context d) throws ResolveError, IOException {
if (parameter != null) {
Type type = builder.toSemanticType(parameter.type, d);
return environment.declare(parameter.name, type, type);
} else {
return environment;
}
}
// =========================================================================
// Misc
// =========================================================================
/**
* This method is provided to properly handled positions which should be
* dead code.
*
* @param context
* --- Context in which bytecodes are executed
*/
private <T> T deadCode(SyntacticElement element) {
throw new InternalFailure("dead code reached",file.getEntry(),element);
}
// Check t1 :> t2
private void checkIsSubtype(Type t1, Type t2, SyntacticElement elem, Environment environment) throws ResolveError {
if (!typeSystem.isSubtype(t1, t2, environment.getLifetimeRelation())) {
throw new SyntaxError(errorMessage(SUBTYPE_ERROR, t1, t2), file.getEntry(), elem);
}
}
private void checkIsSubtype(Type t1, Expr t2, Environment environment) throws ResolveError {
if (!typeSystem.isSubtype(t1, t2.result(), environment.getLifetimeRelation())) {
// We use the nominal type for error reporting, since this includes
// more helpful names.
throw new SyntaxError(errorMessage(SUBTYPE_ERROR, t1, t2.result()), file.getEntry(), t2);
}
}
private void checkIsSubtype(Type t1, Expr t2, Context context, Environment environment) throws ResolveError {
if (!typeSystem.isSubtype(t1, t2.result(), environment.getLifetimeRelation())) {
// We use the nominal type for error reporting, since this includes
// more helpful names.
syntaxError(errorMessage(SUBTYPE_ERROR, t1, t2.result()), context, t2);
}
}
// Check t1 <: t2 or t1 <: t3 ...
private void checkSuptypes(Expr e, Context context, Environment environment, Type... types) throws ResolveError {
Type t1 = e.result();
for (Type t : types) {
if (typeSystem.isSubtype(t, t1, environment.getLifetimeRelation())) {
return; // OK
}
}
// Construct the message
String msg = "expecting ";
boolean firstTime = true;
for (Type t : types) {
if (!firstTime) {
msg += " or ";
}
firstTime = false;
msg = msg + t;
}
msg += ", found " + t1;
syntaxError(msg, context, e);
}
// ==========================================================================
// Environment Class
// ==========================================================================
/**
* <p>
* Responsible for mapping source-level variables to their declared and
* actual types, at any given program point. Since the flow-type checker
* uses a flow-sensitive approach to type checking, then the typing
* environment will change as we move through the statements of a function
* or method.
* </p>
*
* <p>
* This class is implemented in a functional style to minimise possible
* problems related to aliasing (which have been a problem in the past). To
* improve performance, reference counting is to ensure that cloning the
* underling map is only performed when actually necessary.
* </p>
*
* @author David J. Pearce
*
*/
private static final class Environment {
/**
* The mapping of variables to their declared type.
*/
private final HashMap<String, Type> declaredTypes;
/**
* The mapping of variables to their current type.
*/
private final HashMap<String, Type> currentTypes;
/**
* The reference count, which indicate how many references to this
* environment there are. When there is only one reference, then the put
* and putAll operations will perform an "inplace" update (i.e. without
* cloning the underlying collection).
*/
private int count; // refCount
/**
* Whether we are currently inside a Lambda body
*/
private boolean inLambda;
/**
* The lifetimes that are allowed to be dereferenced in a lambda body.
* These are lifetime parameters to the lambda expression and declared
* context lifetimes.
*/
private final HashSet<String> lambdaLifetimes;
/**
* The lifetime relation remembers how lifetimes are ordered (they are
* in a partial order).
*/
private final LifetimeRelation lifetimeRelation;
/**
* Construct an empty environment. Initially the reference count is 1.
*/
public Environment() {
count = 1;
currentTypes = new HashMap<>();
declaredTypes = new HashMap<>();
inLambda = false;
lambdaLifetimes = new HashSet<>();
lifetimeRelation = new LifetimeRelation();
}
/**
* Construct a fresh environment as a copy of another map. Initially the
* reference count is 1.
*/
private Environment(Environment environment) {
count = 1;
this.currentTypes = (HashMap<String, Type>) environment.currentTypes.clone();
this.declaredTypes = (HashMap<String, Type>) environment.declaredTypes.clone();
inLambda = environment.inLambda;
lambdaLifetimes = (HashSet<String>) environment.lambdaLifetimes.clone();
lifetimeRelation = new LifetimeRelation(environment.lifetimeRelation);
}
/**
* Get the type associated with a given variable at the current program
* point, or null if that variable is not declared.
*
* @param variable
* Variable to return type for.
* @return
*/
public Type getCurrentType(String variable) {
return currentTypes.get(variable);
}
/**
* Get the declared type of a given variable, or null if that variable
* is not declared.
*
* @param variable
* Variable to return type for.
* @return
*/
public Type getDeclaredType(String variable) {
return declaredTypes.get(variable);
}
/**
* Check whether a given variable is declared within this environment.
*
* @param variable
* @return
*/
public boolean containsKey(String variable) {
return declaredTypes.containsKey(variable);
}
/**
* Return the set of declared variables in this environment (a.k.a the
* domain).
*
* @return
*/
public Set<String> keySet() {
return declaredTypes.keySet();
}
/**
* Declare a new variable with a given type. In the case that this
* environment has a reference count of 1, then an "in place" update is
* performed. Otherwise, a fresh copy of this environment is returned
* with the given variable associated with the given type, whilst this
* environment is unchanged.
*
* @param variable
* Name of variable to be declared with given type
* @param declared
* Declared type of the given variable
* @param initial
* Initial type of given variable
* @return An updated version of the environment which contains the new
* association.
*/
public Environment declare(String variable, Type declared, Type initial) {
// TODO: check that lifetimes and variables are disjoint
if (declaredTypes.containsKey(variable)) {
throw new RuntimeException("Variable already declared - " + variable);
}
if (count == 1) {
declaredTypes.put(variable, declared);
currentTypes.put(variable, initial);
return this;
} else {
Environment nenv = new Environment(this);
nenv.declaredTypes.put(variable, declared);
nenv.currentTypes.put(variable, initial);
count--;
return nenv;
}
}
/**
* Declare lifetime parameters for a method. In the case that this
* environment has a reference count of 1, then an "in place" update is
* performed. Otherwise, a fresh copy of this environment is returned
* with the given variable associated with the given type, whilst this
* environment is unchanged.
*
* @param lifetimeParameters
* @return An updated version of the environment which contains the
* lifetime parameters.
*/
public Environment declareLifetimeParameters(List<String> lifetimeParameters) {
// TODO: check duplicated variable/lifetime names
if (count == 1) {
this.lifetimeRelation.addParameters(lifetimeParameters);
return this;
} else {
Environment nenv = new Environment(this);
nenv.lifetimeRelation.addParameters(lifetimeParameters);
count--;
return nenv;
}
}
/**
* Declare a lifetime for a named block. In the case that this
* environment has a reference count of 1, then an "in place" update is
* performed. Otherwise, a fresh copy of this environment is returned
* with the given variable associated with the given type, whilst this
* environment is unchanged.
*
* @param lifetime
* @return An updated version of the environment which contains the
* named block.
*/
public Environment startNamedBlock(String lifetime) {
// TODO: check duplicated variable/lifetime names
if (count == 1) {
this.lifetimeRelation.startNamedBlock(lifetime);
return this;
} else {
Environment nenv = new Environment(this);
nenv.lifetimeRelation.startNamedBlock(lifetime);
count--;
return nenv;
}
}
/**
* End the last named block, i.e. remove its declared lifetime. In the
* case that this environment has a reference count of 1, then an
* "in place" update is performed. Otherwise, a fresh copy of this
* environment is returned with the given variable associated with the
* given type, whilst this environment is unchanged.
*
* @param lifetime
* @return An updated version of the environment without the given named
* block.
*/
public Environment endNamedBlock(String lifetime) {
if (count == 1) {
this.lifetimeRelation.endNamedBlock(lifetime);
return this;
} else {
Environment nenv = new Environment(this);
nenv.lifetimeRelation.endNamedBlock(lifetime);
count--;
return nenv;
}
}
/**
* Update the current type of a given variable. If that variable already
* had a current type, then this is overwritten. In the case that this
* environment has a reference count of 1, then an "in place" update is
* performed. Otherwise, a fresh copy of this environment is returned
* with the given variable associated with the given type, whilst this
* environment is unchanged.
*
* @param variable
* Name of variable to be associated with given type
* @param type
* Type to associated with given variable
* @return An updated version of the environment which contains the new
* association.
*/
public Environment update(String variable, Type type) {
if (!declaredTypes.containsKey(variable)) {
throw new RuntimeException("Variable not declared - " + variable);
}
if (count == 1) {
currentTypes.put(variable, type);
return this;
} else {
Environment nenv = new Environment(this);
nenv.currentTypes.put(variable, type);
count--;
return nenv;
}
}
/**
* Remove a variable and any associated type from this environment. In
* the case that this environment has a reference count of 1, then an
* "in place" update is performed. Otherwise, a fresh copy of this
* environment is returned with the given variable and any association
* removed.
*
* @param variable
* Name of variable to be removed from the environment
* @return An updated version of the environment in which the given
* variable no longer exists.
*/
public Environment remove(String key) {
if (count == 1) {
declaredTypes.remove(key);
currentTypes.remove(key);
return this;
} else {
Environment nenv = new Environment(this);
nenv.currentTypes.remove(key);
nenv.declaredTypes.remove(key);
count--;
return nenv;
}
}
/**
* Create a fresh copy of this environment, but set the lambda flag and
* remember the given context lifetimes and lifetime parameters.
*
* @param contextLifetimes
* the declared context lifetimes
* @param lifetimeParameters
* The lifetime names that are allowed to be dereferenced
* inside the lambda.
*/
public Environment startLambda(Collection<String> contextLifetimes, Collection<String> lifetimeParameters) {
Environment nenv = new Environment(this);
nenv.inLambda = true;
nenv.lambdaLifetimes.clear();
nenv.lambdaLifetimes.addAll(contextLifetimes);
nenv.lambdaLifetimes.addAll(lifetimeParameters);
return nenv;
}
/**
* Check whether we are allowed to dereference the given lifetime.
* Inside a lambda, only "*", the declared context lifetimes and the
* lifetime parameters can be dereferenced.
*
* @param lifetime
* @return
*/
public boolean canDereferenceLifetime(String lifetime) {
return !inLambda || lifetime.equals("*") || lambdaLifetimes.contains(lifetime);
}
/**
* Get the current lifetime relation.
*
* @return
*/
public LifetimeRelation getLifetimeRelation() {
return this.lifetimeRelation;
}
/**
* Merge a given environment with this environment to produce an
* environment representing their join. Only variables from a given set
* are included in the result, and all such variables are required to be
* declared in both environments. The type of each variable included is
* the union of its type in this environment and the other environment.
*
* @param declared
* The set of declared variables which should be included in
* the result. The intuition is that these are the variables
* which were declared in both environments before whatever
* updates were made.
* @param env
* The given environment to be merged with this environment.
* @return
*/
public final Environment merge(Set<String> declared, Environment env) {
// first, need to check for the special bottom value case.
if (this == BOTTOM) {
return env;
} else if (env == BOTTOM) {
return this;
}
// ok, not bottom so compute intersection.
this.free();
env.free();
Environment result = new Environment();
for (String variable : declared) {
Type lhs_t = this.getCurrentType(variable);
Type rhs_t = env.getCurrentType(variable);
result.declare(variable, this.getDeclaredType(variable), Type.Union(lhs_t, rhs_t));
}
result.lifetimeRelation.replaceWithMerge(this.lifetimeRelation, env.lifetimeRelation);
return result;
}
/**
* Create a fresh copy of this environment. In fact, this operation
* simply increments the reference count of this environment and returns
* it.
*/
@Override
public Environment clone() {
count++;
return this;
}
/**
* Decrease the reference count of this environment by one.
*/
public void free() {
--count;
}
@Override
public String toString() {
return currentTypes.toString();
}
@Override
public int hashCode() {
return 31 * currentTypes.hashCode() + lambdaLifetimes.hashCode();
}
@Override
public boolean equals(Object o) {
if (o instanceof Environment) {
Environment r = (Environment) o;
return currentTypes.equals(r.currentTypes) && lambdaLifetimes.equals(r.lambdaLifetimes);
}
return false;
}
}
private static final Environment BOTTOM = new Environment();
}