org.eclipse.jdt.internal.compiler.lookup.InferenceContext18.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jdt.internal.compiler.lookup.InferenceContext18.java

Source

/*******************************************************************************
 * Copyright (c) 2013, 2019 GK Software AG, and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Stephan Herrmann - initial API and implementation
 *     IBM Corporation - Bug fixes
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.lookup;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.ConditionalExpression;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FunctionalExpression;
import org.eclipse.jdt.internal.compiler.ast.Invocation;
import org.eclipse.jdt.internal.compiler.ast.LambdaExpression;
import org.eclipse.jdt.internal.compiler.ast.ReferenceExpression;
import org.eclipse.jdt.internal.compiler.ast.SwitchExpression;
import org.eclipse.jdt.internal.compiler.ast.Wildcard;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants.BoundCheckStatus;
import org.eclipse.jdt.internal.compiler.util.Sorting;

/**
 * Main class for new type inference as per JLS8 sect 18.
 * Keeps contextual state and drives the algorithm.
 * 
 * <h2>Inference Basics</h2>
 * <ul>
 * <li>18.1.1 Inference variables: {@link InferenceVariable}</li>
 * <li>18.1.2 Constraint Formulas: subclasses of {@link ConstraintFormula}</li>
 * <li>18.1.3 Bounds: {@link TypeBound}<br/>
 *    Capture bounds are directly captured in {@link BoundSet#captures}, throws-bounds in {@link BoundSet#inThrows}.<br/>
 *    Also: {@link BoundSet}: main state during inference.</li>
 * </ul>
 * Each instance of {@link InferenceContext18} manages instances of the above and coordinates the inference process.
 * <h3>Queries and utilities</h3>
 * <ul>
 * <li>{@link TypeBinding#isProperType(boolean)}:
 *     used to exclude "types" that mention inference variables (18.1.1).</li>
 * <li>{@link TypeBinding#mentionsAny(TypeBinding[], int)}:
 *     does the receiver type binding mention any of the given types?</li>
 * <li>{@link TypeBinding#substituteInferenceVariable(InferenceVariable, TypeBinding)}:
 *     replace occurrences of an inference variable with a proper type.</li>
 * <li>{@link TypeBinding#collectInferenceVariables(Set)}:
 *     collect all inference variables mentioned in the receiver type into the given set.</li>
 * <li>{@link TypeVariableBinding#getTypeBounds(InferenceVariable, InferenceSubstitution)}:
 *    Compute the initial type bounds for one inference variable as per JLS8 sect 18.1.3.</li>
 * </ul>
 * <h2>Phases of Inference</h2>
 * <ul>
 * <li>18.2 <b>Reduction</b>: {@link #reduce()} with most work happening in implementations of
 *  {@link ConstraintFormula#reduce(InferenceContext18)}:
 *  <ul>
 *  <li>18.2.1 Expression Compatibility Constraints: {@link ConstraintExpressionFormula#reduce(InferenceContext18)}.</li>
 *  <li>18.2.2 Type Compatibility Constraints ff. {@link ConstraintTypeFormula#reduce(InferenceContext18)}.</li>
 *  </ul></li>
 * <li>18.3 <b>Incorporation</b>: {@link BoundSet#incorporate(InferenceContext18)}; during inference new constraints
 *    are accepted via {@link BoundSet#reduceOneConstraint(InferenceContext18, ConstraintFormula)} (combining 18.2 & 18.3)</li>
 * <li>18.4 <b>Resolution</b>: {@link #resolve(InferenceVariable[])}.
 * </ul>
 * Some of the above operations accumulate their results into {@link #currentBounds}, whereas
 * the last phase <em>returns</em> the resulting bound set while keeping the previous state in {@link #currentBounds}.
 * <h2>18.5. Uses of Inference</h2>
 * These are the main entries from the compiler into the inference engine:
 * <dl>
 * <dt>18.5.1 Invocation Applicability Inference</dt>
 * <dd>{@link #inferInvocationApplicability(MethodBinding, TypeBinding[], boolean)}. Prepare the initial state for
 *    inference of a generic invocation - no target type used at this point.
 *  Need to call {@link #solve(boolean)} with true afterwards to produce the intermediate result.<br/>
 *  Called indirectly from {@link Scope#findMethod(ReferenceBinding, char[], TypeBinding[], InvocationSite, boolean)} et al
 *  to select applicable methods into overload resolution.</dd>
 * <dt>18.5.2 Invocation Type Inference</dt>
 * <dd>{@link InferenceContext18#inferInvocationType(TypeBinding, InvocationSite, MethodBinding)}. After a
 *    most specific method has been picked, and given a target type determine the final generic instantiation.
 *  As long as a target type is still unavailable this phase keeps getting deferred.</br>
 *  Different wrappers exist for the convenience of different callers.</dd>
 * <dt>18.5.3 Functional Interface Parameterization Inference</dt>
 * <dd>Controlled from {@link LambdaExpression#resolveType(BlockScope)}.</dd>
 * <dt>18.5.4 More Specific Method Inference</dt>
 * <dd><em>Not Yet Implemented</em></dd>
 * </dl>
 * For 18.5.1 and 18.5.2 high-level control is implemented in
 *  {@link ParameterizedGenericMethodBinding#computeCompatibleMethod(MethodBinding, TypeBinding[], Scope, InvocationSite)}.
 * <h2>Inference Lifecycle</h2>
 * <li>Decision whether or not an invocation is a <b>variable-arity</b> invocation is made by first attempting
 *       to solve 18.5.1 in mode {@link #CHECK_LOOSE}. Only if that fails, another attempt is made in mode {@link #CHECK_VARARG}.
 *       Which of these two attempts was successful is stored in {@link #inferenceKind}.
 *       This field must be consulted whenever arguments of an invocation should be further processed.
 *       See also {@link #getParameter(TypeBinding[], int, boolean)} and its clients.</li>
 * </ul>
 */
public class InferenceContext18 {

    /** to conform with javac regarding https://bugs.openjdk.java.net/browse/JDK-8026527 */
    static final boolean SIMULATE_BUG_JDK_8026527 = true;

    /** Temporary workaround until we know fully what to do with https://bugs.openjdk.java.net/browse/JDK-8054721 
     *  It looks likely that we have a bug independent of this JLS bug in that we clear the capture bounds eagerly.
    */
    static final boolean SHOULD_WORKAROUND_BUG_JDK_8054721 = true; // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=437444#c24 onwards

    static final boolean SHOULD_WORKAROUND_BUG_JDK_8153748 = true; // emulating javac behaviour after private email communication

    /**
     * Detail flag to control the extent of {@link #SIMULATE_BUG_JDK_8026527}.
     * A setting of 'false' implements the advice from http://mail.openjdk.java.net/pipermail/lambda-spec-experts/2013-December/000447.html
     * i.e., raw types are not considered as compatible in constraints/bounds derived from invocation arguments,
     * but only for constraints derived from type variable bounds.
     */
    static final boolean ARGUMENT_CONSTRAINTS_ARE_SOFT = false;

    // --- Main State of the Inference: ---

    /** the invocation being inferred (for 18.5.1 and 18.5.2) */
    InvocationSite currentInvocation;
    /** arguments of #currentInvocation, if any */
    Expression[] invocationArguments;

    /** The inference variables for which as solution is sought. */
    InferenceVariable[] inferenceVariables;

    /** Constraints that have not yet been reduced and incorporated. */
    ConstraintFormula[] initialConstraints;
    ConstraintExpressionFormula[] finalConstraints; // for final revalidation at a "macroscopic" level

    /** The accumulated type bounds etc. */
    BoundSet currentBounds;

    /** One of CHECK_STRICT, CHECK_LOOSE, or CHECK_VARARGS. */
    int inferenceKind;
    /** Marks how much work has been done so far? Used to avoid performing any of these tasks more than once. */
    public int stepCompleted = NOT_INFERRED;

    public static final int NOT_INFERRED = 0;
    /** Applicability Inference (18.5.1) has been completed. */
    public static final int APPLICABILITY_INFERRED = 1;
    /** Invocation Type Inference (18.5.2) has been completed (for some target type). */
    public static final int TYPE_INFERRED = 2;
    public static final int TYPE_INFERRED_FINAL = 3; // as above plus asserting that target type was a proper type

    /** Signals whether any type compatibility makes use of unchecked conversion. */
    public List<ConstraintFormula> constraintsWithUncheckedConversion;
    public boolean usesUncheckedConversion;
    public InferenceContext18 outerContext;
    private Set<InferenceContext18> seenInnerContexts;
    Scope scope;
    LookupEnvironment environment;
    ReferenceBinding object; // java.lang.Object
    public BoundSet b2;
    private BoundSet b3;
    /** Not per JLS: inbox for emulation of how javac passes type bounds from inner to outer */
    private BoundSet innerInbox;
    /** Not per JLS: signal when current is ready to directly merge all bounds from inner. */
    private boolean directlyAcceptingInnerBounds = false;
    /** Not per JLS: pushing bounds from inner to outer may have to be deferred till after overload resolution, store here a runnable to perform the push. */
    private Runnable pushToOuterJob = null;

    public static boolean isSameSite(InvocationSite site1, InvocationSite site2) {
        if (site1 == site2)
            return true;
        if (site1 == null || site2 == null)
            return false;
        if (site1.sourceStart() == site2.sourceStart() && site1.sourceEnd() == site2.sourceEnd())
            return true;
        return false;
    }

    public static final int CHECK_UNKNOWN = 0;
    public static final int CHECK_STRICT = 1;
    public static final int CHECK_LOOSE = 2;
    public static final int CHECK_VARARG = 3;

    static class SuspendedInferenceRecord {
        InvocationSite site;
        Expression[] invocationArguments;
        InferenceVariable[] inferenceVariables;
        int inferenceKind;
        boolean usesUncheckedConversion;

        SuspendedInferenceRecord(InvocationSite site, Expression[] invocationArguments,
                InferenceVariable[] inferenceVariables, int inferenceKind, boolean usesUncheckedConversion) {
            this.site = site;
            this.invocationArguments = invocationArguments;
            this.inferenceVariables = inferenceVariables;
            this.inferenceKind = inferenceKind;
            this.usesUncheckedConversion = usesUncheckedConversion;
        }
    }

    /** Construct an inference context for an invocation (method/constructor). */
    public InferenceContext18(Scope scope, Expression[] arguments, InvocationSite site,
            InferenceContext18 outerContext) {
        this.scope = scope;
        this.environment = scope.environment();
        this.object = scope.getJavaLangObject();
        this.invocationArguments = arguments;
        this.currentInvocation = site;
        this.outerContext = outerContext;
        if (site instanceof Invocation)
            scope.compilationUnitScope().registerInferredInvocation((Invocation) site);
    }

    public InferenceContext18(Scope scope) {
        this.scope = scope;
        this.environment = scope.environment();
        this.object = scope.getJavaLangObject();
    }

    /**
     * JLS 18.1.3: Create initial bounds from a given set of type parameters declarations.
     * @return the set of inference variables created for the given typeParameters
     */
    public InferenceVariable[] createInitialBoundSet(TypeVariableBinding[] typeParameters) {
        // 
        if (this.currentBounds == null) {
            this.currentBounds = new BoundSet();
        }
        if (typeParameters != null) {
            InferenceVariable[] newInferenceVariables = addInitialTypeVariableSubstitutions(typeParameters);
            this.currentBounds.addBoundsFromTypeParameters(this, typeParameters, newInferenceVariables);
            return newInferenceVariables;
        }
        return Binding.NO_INFERENCE_VARIABLES;
    }

    /**
     * Substitute any type variables mentioned in 'type' by the corresponding inference variable, if one exists. 
     */
    public TypeBinding substitute(TypeBinding type) {
        InferenceSubstitution inferenceSubstitution = new InferenceSubstitution(this);
        return inferenceSubstitution.substitute(inferenceSubstitution, type);
    }

    /** JLS 18.5.1: compute bounds from formal and actual parameters. */
    public void createInitialConstraintsForParameters(TypeBinding[] parameters, boolean checkVararg,
            TypeBinding varArgsType, MethodBinding method) {
        if (this.invocationArguments == null)
            return;
        int len = checkVararg ? parameters.length - 1
                : Math.min(parameters.length, this.invocationArguments.length);
        int maxConstraints = checkVararg ? this.invocationArguments.length : len;
        int numConstraints = 0;
        boolean ownConstraints;
        if (this.initialConstraints == null) {
            this.initialConstraints = new ConstraintFormula[maxConstraints];
            ownConstraints = true;
        } else {
            numConstraints = this.initialConstraints.length;
            maxConstraints += numConstraints;
            System.arraycopy(this.initialConstraints, 0,
                    this.initialConstraints = new ConstraintFormula[maxConstraints], 0, numConstraints);
            ownConstraints = false; // these are lifted from a nested poly expression.
        }
        for (int i = 0; i < len; i++) {
            TypeBinding thetaF = substitute(parameters[i]);
            if (this.invocationArguments[i].isPertinentToApplicability(parameters[i], method)) {
                this.initialConstraints[numConstraints++] = new ConstraintExpressionFormula(
                        this.invocationArguments[i], thetaF, ReductionResult.COMPATIBLE,
                        ARGUMENT_CONSTRAINTS_ARE_SOFT);
            } else if (!isTypeVariableOfCandidate(parameters[i], method)) {
                this.initialConstraints[numConstraints++] = new ConstraintExpressionFormula(
                        this.invocationArguments[i], thetaF, ReductionResult.POTENTIALLY_COMPATIBLE);
            } // else we know it is potentially compatible, no need to assert.
        }
        if (checkVararg && varArgsType instanceof ArrayBinding) {
            varArgsType = ((ArrayBinding) varArgsType).elementsType();
            TypeBinding thetaF = substitute(varArgsType);
            for (int i = len; i < this.invocationArguments.length; i++) {
                if (this.invocationArguments[i].isPertinentToApplicability(varArgsType, method)) {
                    this.initialConstraints[numConstraints++] = new ConstraintExpressionFormula(
                            this.invocationArguments[i], thetaF, ReductionResult.COMPATIBLE,
                            ARGUMENT_CONSTRAINTS_ARE_SOFT);
                } else if (!isTypeVariableOfCandidate(varArgsType, method)) {
                    this.initialConstraints[numConstraints++] = new ConstraintExpressionFormula(
                            this.invocationArguments[i], thetaF, ReductionResult.POTENTIALLY_COMPATIBLE);
                } // else we know it is potentially compatible, no need to assert.
            }
        }
        if (numConstraints == 0)
            this.initialConstraints = ConstraintFormula.NO_CONSTRAINTS;
        else if (numConstraints < maxConstraints)
            System.arraycopy(this.initialConstraints, 0,
                    this.initialConstraints = new ConstraintFormula[numConstraints], 0, numConstraints);
        if (ownConstraints) { // lifted constraints get validated at their own context.
            final int length = this.initialConstraints.length;
            System.arraycopy(this.initialConstraints, 0,
                    this.finalConstraints = new ConstraintExpressionFormula[length], 0, length);
        }
    }

    private boolean isTypeVariableOfCandidate(TypeBinding type, MethodBinding candidate) {
        // cf. FunctionalExpression.isPertinentToApplicability()
        if (type instanceof TypeVariableBinding) {
            Binding declaringElement = ((TypeVariableBinding) type).declaringElement;
            if (declaringElement == candidate)
                return true;
            if (candidate.isConstructor() && declaringElement == candidate.declaringClass)
                return true;
        }
        return false;
    }

    private InferenceVariable[] addInitialTypeVariableSubstitutions(TypeBinding[] typeVariables) {
        int len = typeVariables.length;
        if (len == 0) {
            if (this.inferenceVariables == null)
                this.inferenceVariables = Binding.NO_INFERENCE_VARIABLES;
            return Binding.NO_INFERENCE_VARIABLES;
        }
        InferenceVariable[] newVariables = new InferenceVariable[len];
        for (int i = 0; i < len; i++)
            newVariables[i] = InferenceVariable.get(typeVariables[i], i, this.currentInvocation, this.scope,
                    this.object, true);
        addInferenceVariables(newVariables);
        return newVariables;
    }

    private void addInferenceVariables(InferenceVariable[] newVariables) {
        if (this.inferenceVariables == null || this.inferenceVariables.length == 0) {
            this.inferenceVariables = newVariables;
        } else {
            // merge into this.inferenceVariables:
            int len = newVariables.length;
            int prev = this.inferenceVariables.length;
            System.arraycopy(this.inferenceVariables, 0,
                    this.inferenceVariables = new InferenceVariable[len + prev], 0, prev);
            System.arraycopy(newVariables, 0, this.inferenceVariables, prev, len);
        }
    }

    /** Add new inference variables for the given type variables. */
    public InferenceVariable[] addTypeVariableSubstitutions(TypeBinding[] typeVariables) {
        int len2 = typeVariables.length;
        InferenceVariable[] newVariables = new InferenceVariable[len2];
        InferenceVariable[] toAdd = new InferenceVariable[len2];
        int numToAdd = 0;
        for (int i = 0; i < typeVariables.length; i++) {
            if (typeVariables[i] instanceof InferenceVariable)
                newVariables[i] = (InferenceVariable) typeVariables[i]; // prevent double substitution of an already-substituted inferenceVariable
            else
                toAdd[numToAdd++] = newVariables[i] = InferenceVariable.get(typeVariables[i], i,
                        this.currentInvocation, this.scope, this.object, false);
        }
        if (numToAdd > 0) {
            int start = 0;
            if (this.inferenceVariables != null) {
                int len1 = this.inferenceVariables.length;
                System.arraycopy(this.inferenceVariables, 0,
                        this.inferenceVariables = new InferenceVariable[len1 + numToAdd], 0, len1);
                start = len1;
            } else {
                this.inferenceVariables = new InferenceVariable[numToAdd];
            }
            System.arraycopy(toAdd, 0, this.inferenceVariables, start, numToAdd);
        }
        return newVariables;
    }

    /** JLS 18.1.3 Bounds: throws : the inference variable  appears in a throws clause */
    public void addThrowsContraints(TypeBinding[] parameters, InferenceVariable[] variables,
            ReferenceBinding[] thrownExceptions) {
        for (int i = 0; i < parameters.length; i++) {
            TypeBinding parameter = parameters[i];
            for (int j = 0; j < thrownExceptions.length; j++) {
                if (TypeBinding.equalsEquals(parameter, thrownExceptions[j])) {
                    this.currentBounds.inThrows.add(variables[i].prototype());
                    break;
                }
            }
        }
    }

    /** JLS 18.5.1 Invocation Applicability Inference. */
    public void inferInvocationApplicability(MethodBinding method, TypeBinding[] arguments, boolean isDiamond) {
        ConstraintExpressionFormula.inferInvocationApplicability(this, method, arguments, isDiamond,
                this.inferenceKind);
    }

    /** Perform steps from JLS 18.5.2. needed for computing the bound set B3. */
    boolean computeB3(InvocationSite invocationSite, TypeBinding targetType, MethodBinding method)
            throws InferenceFailureException {
        boolean result = ConstraintExpressionFormula.inferPolyInvocationType(this, invocationSite, targetType,
                method);
        if (result) {
            mergeInnerBounds();
            if (this.b3 == null)
                this.b3 = this.currentBounds.copy();
        }
        return result;
    }

    /** JLS 18.5.2 Invocation Type Inference 
     */
    public BoundSet inferInvocationType(TypeBinding expectedType, InvocationSite invocationSite,
            MethodBinding method) throws InferenceFailureException {
        // not JLS: simply ensure that null hints from the return type have been seen even in standalone contexts:
        if (expectedType == null && method.returnType != null)
            substitute(method.returnType); // result is ignore, the only effect is on InferenceVariable.nullHints

        this.currentBounds = this.b2.copy();

        int step = (expectedType == null || expectedType.isProperType(true)) ? TYPE_INFERRED_FINAL : TYPE_INFERRED;

        try {
            // bullets 1&2: definitions only.
            if (expectedType != null && expectedType != TypeBinding.VOID && invocationSite instanceof Expression
                    && ((Expression) invocationSite).isTrulyExpression()
                    && ((Expression) invocationSite).isPolyExpression(method)) {
                // 3. bullet: special treatment for poly expressions
                if (!computeB3(invocationSite, expectedType, method)) {
                    return null;
                }
            } else {
                mergeInnerBounds();
                this.b3 = this.currentBounds.copy();
            }

            if (SHOULD_WORKAROUND_BUG_JDK_8153748) { // "before 18.5.2", but should not spill into b3 ... (heuristically)
                ReductionResult jdk8153748result = addJDK_8153748ConstraintsFromInvocation(this.invocationArguments,
                        method, new InferenceSubstitution(this));
                if (jdk8153748result != null) {
                    if (!this.currentBounds.incorporate(this))
                        return null;
                }
            }

            pushBoundsToOuter();
            this.directlyAcceptingInnerBounds = true;

            // 4. bullet: assemble C:
            Set<ConstraintFormula> c = new HashSet<ConstraintFormula>();
            if (!addConstraintsToC(this.invocationArguments, c, method, this.inferenceKind, invocationSite))
                return null;
            // 5. bullet: determine B4 from C
            List<Set<InferenceVariable>> components = this.currentBounds
                    .computeConnectedComponents(this.inferenceVariables);
            while (!c.isEmpty()) {
                // *
                Set<ConstraintFormula> bottomSet = findBottomSet(c, allOutputVariables(c), components);
                if (bottomSet.isEmpty()) {
                    bottomSet.add(pickFromCycle(c));
                }
                // *
                c.removeAll(bottomSet);
                // * The union of the input variables of all the selected constraints, 1, ..., m, ...
                Set<InferenceVariable> allInputs = new HashSet<InferenceVariable>();
                Iterator<ConstraintFormula> bottomIt = bottomSet.iterator();
                while (bottomIt.hasNext()) {
                    allInputs.addAll(bottomIt.next().inputVariables(this));
                }
                InferenceVariable[] variablesArray = allInputs.toArray(new InferenceVariable[allInputs.size()]);
                //   ... is resolved
                if (!this.currentBounds.incorporate(this))
                    return null;
                BoundSet solution = resolve(variablesArray);
                // in rare cases resolving just one set of variables doesn't suffice,
                // don't bother with finding the necessary superset, just resolve all:
                if (solution == null)
                    solution = resolve(this.inferenceVariables);
                // * ~ apply substitutions to all constraints: 
                bottomIt = bottomSet.iterator();
                while (bottomIt.hasNext()) {
                    ConstraintFormula constraint = bottomIt.next();
                    if (solution != null)
                        if (!constraint.applySubstitution(solution, variablesArray))
                            return null;
                    // * reduce and incorporate
                    if (!this.currentBounds.reduceOneConstraint(this, constraint))
                        return null;
                }
            }
            // 6. bullet: solve
            BoundSet solution = solve();
            if (solution == null || !isResolved(solution)) {
                this.currentBounds = this.b2; // don't let bounds from unsuccessful attempt leak into subsequent attempts
                return null;
            }
            // we're done, start reporting:
            reportUncheckedConversions(solution);
            if (step == TYPE_INFERRED_FINAL)
                this.currentBounds = solution; // this is final, keep the result:
            return solution;
        } finally {
            this.stepCompleted = step;
        }
    }

    // ---  not per JLS: emulate how javac passes type bounds from inner to outer: ---
    /** Not per JLS: push current bounds to outer inference if outer is ready for it. */
    private void pushBoundsToOuter() {
        pushBoundsTo(this.outerContext);
    }

    /** Not per JLS: invent more bubbling up of inner bounds. */
    public void pushBoundsTo(InferenceContext18 outer) {
        if (outer != null && outer.stepCompleted >= APPLICABILITY_INFERRED) {
            boolean deferred = outer.currentInvocation instanceof Invocation; // need to wait till after overload resolution?
            BoundSet toPush = deferred ? this.currentBounds.copy() : this.currentBounds;
            Runnable job = () -> {
                if (outer.directlyAcceptingInnerBounds) {
                    outer.currentBounds.addBounds(toPush, this.environment);
                } else if (outer.innerInbox == null) {
                    outer.innerInbox = deferred ? toPush : toPush.copy(); // copy now, unless already copied on behalf of 'deferred'
                } else {
                    outer.innerInbox.addBounds(toPush, this.environment);
                }
            };
            if (deferred) {
                this.pushToOuterJob = job;
            } else {
                job.run(); // TODO(stephan): ever reached? for ReferenceExpression? (would need a corresponding new call to flushBoundOutbox()).
            }
        }
    }

    /** Not JLS: after overload resolution is done, perform the push of type bounds to outer inference, if any. */
    public void flushBoundOutbox() {
        if (this.pushToOuterJob != null) {
            this.pushToOuterJob.run();
            this.pushToOuterJob = null;
        }
    }

    /** Not JLS: merge pending bounds of inner inference into current. */
    private void mergeInnerBounds() {
        if (this.innerInbox != null) {
            this.currentBounds.addBounds(this.innerInbox, this.environment);
            this.innerInbox = null;
        }
    }

    interface InferenceOperation {
        boolean perform() throws InferenceFailureException;
    }

    /** Not per JLS: if operation succeeds merge new bounds from inner into current. */
    private boolean collectingInnerBounds(InferenceOperation operation) throws InferenceFailureException {
        boolean result = operation.perform();
        if (result)
            mergeInnerBounds();
        else
            this.innerInbox = null;
        return result;
    }
    // ---

    private ReductionResult addJDK_8153748ConstraintsFromInvocation(Expression[] arguments, MethodBinding method,
            InferenceSubstitution substitution) throws InferenceFailureException {
        // not per JLS, trying to mimic javac behavior
        boolean constraintAdded = false;
        if (arguments != null) {
            for (int i = 0; i < arguments.length; i++) {
                Expression argument = arguments[i];
                TypeBinding parameter = getParameter(method.parameters, i, method.isVarargs());
                parameter = substitution.substitute(substitution, parameter);
                ReductionResult result = addJDK_8153748ConstraintsFromExpression(argument, parameter, method,
                        substitution);
                if (result == ReductionResult.FALSE)
                    return ReductionResult.FALSE;
                if (result == ReductionResult.TRUE)
                    constraintAdded = true;
            }
        }
        return constraintAdded ? ReductionResult.TRUE : null;
    }

    private ReductionResult addJDK_8153748ConstraintsFromExpression(Expression argument, TypeBinding parameter,
            MethodBinding method, InferenceSubstitution substitution) throws InferenceFailureException {
        if (argument instanceof FunctionalExpression) {
            return addJDK_8153748ConstraintsFromFunctionalExpr((FunctionalExpression) argument, parameter, method);
        } else if (argument instanceof Invocation && argument.isPolyExpression(method)) {
            Invocation invocation = (Invocation) argument;
            Expression[] innerArgs = invocation.arguments();
            MethodBinding innerMethod = invocation.binding();
            if (innerMethod != null && innerMethod.isValidBinding()) {
                substitution = enrichSubstitution(substitution, invocation, innerMethod);
                return addJDK_8153748ConstraintsFromInvocation(innerArgs, innerMethod.shallowOriginal(),
                        substitution);
            }
        } else if (argument instanceof ConditionalExpression) {
            ConditionalExpression ce = (ConditionalExpression) argument;
            if (addJDK_8153748ConstraintsFromExpression(ce.valueIfTrue, parameter, method,
                    substitution) == ReductionResult.FALSE)
                return ReductionResult.FALSE;
            return addJDK_8153748ConstraintsFromExpression(ce.valueIfFalse, parameter, method, substitution);
        } else if (argument instanceof SwitchExpression) {
            SwitchExpression se = (SwitchExpression) argument;
            ReductionResult result = ReductionResult.FALSE;
            for (Expression re : se.resultExpressions) {
                result = addJDK_8153748ConstraintsFromExpression(re, parameter, method, substitution);
                if (result == ReductionResult.FALSE)
                    break;
            }
            return result;
        }
        return null;
    }

    private ReductionResult addJDK_8153748ConstraintsFromFunctionalExpr(FunctionalExpression functionalExpr,
            TypeBinding targetType, MethodBinding method) throws InferenceFailureException {
        if (!functionalExpr.isPertinentToApplicability(targetType, method)) {
            ConstraintFormula exprConstraint = new ConstraintExpressionFormula(functionalExpr, targetType,
                    ReductionResult.COMPATIBLE, ARGUMENT_CONSTRAINTS_ARE_SOFT);
            if (collectingInnerBounds(() -> exprConstraint.inputVariables(this).isEmpty())) { // input variable would signal: not ready for inference
                if (!collectingInnerBounds(() -> reduceAndIncorporate(exprConstraint)))
                    return ReductionResult.FALSE;
                ConstraintFormula excConstraint = new ConstraintExceptionFormula(functionalExpr, targetType); // ??
                if (!collectingInnerBounds(() -> reduceAndIncorporate(excConstraint)))
                    return ReductionResult.FALSE;
                return ReductionResult.TRUE;
            }
        }
        return null;
    }

    InferenceSubstitution enrichSubstitution(InferenceSubstitution substitution, Invocation innerInvocation,
            MethodBinding innerMethod) {
        if (innerMethod instanceof ParameterizedGenericMethodBinding) {
            InferenceContext18 innerContext = innerInvocation
                    .getInferenceContext((ParameterizedMethodBinding) innerMethod);
            if (innerContext != null)
                return substitution.addContext(innerContext);
        }
        return substitution;
    }

    private boolean addConstraintsToC(Expression[] exprs, Set<ConstraintFormula> c, MethodBinding method,
            int inferenceKindForMethod, InvocationSite site) throws InferenceFailureException {
        TypeBinding[] fs;
        if (exprs != null) {
            int k = exprs.length;
            int p = method.parameters.length;
            if (method.isVarargs()) {
                if (k < p - 1)
                    return false;
            } else if (k != p) {
                return false;
            }
            switch (inferenceKindForMethod) {
            case CHECK_STRICT:
            case CHECK_LOOSE:
                fs = method.parameters;
                break;
            case CHECK_VARARG:
                fs = varArgTypes(method.parameters, k);
                break;
            default:
                throw new IllegalStateException("Unexpected checkKind " + this.inferenceKind); //$NON-NLS-1$
            }
            for (int i = 0; i < k; i++) {
                TypeBinding fsi = fs[Math.min(i, p - 1)];
                InferenceSubstitution inferenceSubstitution = new InferenceSubstitution(this.environment,
                        this.inferenceVariables, site);
                TypeBinding substF = inferenceSubstitution.substitute(inferenceSubstitution, fsi);
                if (!addConstraintsToC_OneExpr(exprs[i], c, fsi, substF, method))
                    return false;
            }
        }
        return true;
    }

    private boolean addConstraintsToC_OneExpr(Expression expri, Set<ConstraintFormula> c, TypeBinding fsi,
            TypeBinding substF, MethodBinding method) throws InferenceFailureException {
        boolean substFIsProperType = substF.isProperType(true);
        // -- not per JLS, emulate javac behavior:
        substF = Scope.substitute(getResultSubstitution(this.b3), substF);
        // --

        // For all i (1  i  k), if ei is not pertinent to applicability, the set contains ei   Fi.
        if (!expri.isPertinentToApplicability(fsi, method)) {
            c.add(new ConstraintExpressionFormula(expri, substF, ReductionResult.COMPATIBLE,
                    ARGUMENT_CONSTRAINTS_ARE_SOFT));
        }
        if (expri instanceof FunctionalExpression) {
            c.add(new ConstraintExceptionFormula((FunctionalExpression) expri, substF));
            if (expri instanceof LambdaExpression) {
                // https://bugs.openjdk.java.net/browse/JDK-8038747
                LambdaExpression lambda = (LambdaExpression) expri;
                BlockScope skope = lambda.enclosingScope;
                if (substF.isFunctionalInterface(skope)) { // could be an inference variable.
                    ReferenceBinding t = (ReferenceBinding) substF;
                    ParameterizedTypeBinding withWildCards = InferenceContext18.parameterizedWithWildcard(t);
                    if (withWildCards != null) {
                        t = ConstraintExpressionFormula.findGroundTargetType(this, skope, lambda, withWildCards);
                    }
                    MethodBinding functionType;
                    if (t != null && (functionType = t.getSingleAbstractMethod(skope, true)) != null
                            && (lambda = lambda.resolveExpressionExpecting(t, this.scope, this)) != null) {
                        TypeBinding r = functionType.returnType;
                        Expression[] resultExpressions = lambda.resultExpressions();
                        for (int i = 0, length = resultExpressions == null ? 0
                                : resultExpressions.length; i < length; i++) {
                            Expression resultExpression = resultExpressions[i];
                            if (!addConstraintsToC_OneExpr(resultExpression, c, r.original(), r, method))
                                return false;
                        }
                    }
                }
            }
        } else if (expri instanceof Invocation && expri.isPolyExpression()) {

            if (substFIsProperType) // https://bugs.openjdk.java.net/browse/JDK-8052325
                return true;

            Invocation invocation = (Invocation) expri;
            MethodBinding innerMethod = invocation.binding();
            if (innerMethod == null)
                return true; // -> proceed with no new C set elements.

            Expression[] arguments = invocation.arguments();
            TypeBinding[] argumentTypes = arguments == null ? Binding.NO_PARAMETERS
                    : new TypeBinding[arguments.length];
            for (int i = 0; i < argumentTypes.length; i++)
                argumentTypes[i] = arguments[i].resolvedType;
            InferenceContext18 innerContext = null;
            if (innerMethod instanceof ParameterizedGenericMethodBinding)
                innerContext = invocation.getInferenceContext((ParameterizedGenericMethodBinding) innerMethod);

            if (innerContext != null) {
                MethodBinding shallowMethod = innerMethod.shallowOriginal();
                innerContext.outerContext = this;
                if (innerContext.stepCompleted < InferenceContext18.APPLICABILITY_INFERRED) // shouldn't happen, but let's play safe
                    innerContext.inferInvocationApplicability(shallowMethod, argumentTypes,
                            shallowMethod.isConstructor());
                if (!innerContext.computeB3(invocation, substF, shallowMethod))
                    return false;
                if (innerContext.addConstraintsToC(arguments, c, innerMethod.genericMethod(),
                        innerContext.inferenceKind, invocation)) {
                    this.currentBounds.addBounds(innerContext.currentBounds, this.environment);
                    return true;
                }
                return false;
            } else {
                int applicabilityKind = getInferenceKind(innerMethod, argumentTypes);
                return this.addConstraintsToC(arguments, c, innerMethod.genericMethod(), applicabilityKind,
                        invocation);
            }
        } else if (expri instanceof ConditionalExpression) {
            ConditionalExpression ce = (ConditionalExpression) expri;
            return addConstraintsToC_OneExpr(ce.valueIfTrue, c, fsi, substF, method)
                    && addConstraintsToC_OneExpr(ce.valueIfFalse, c, fsi, substF, method);
        } else if (expri instanceof SwitchExpression) {
            SwitchExpression se = (SwitchExpression) expri;
            for (Expression re : se.resultExpressions) {
                if (!addConstraintsToC_OneExpr(re, c, fsi, substF, method))
                    return false;
            }
            return true;
        }
        return true;
    }

    protected int getInferenceKind(MethodBinding nonGenericMethod, TypeBinding[] argumentTypes) {
        switch (this.scope.parameterCompatibilityLevel(nonGenericMethod, argumentTypes)) {
        case Scope.AUTOBOX_COMPATIBLE:
            return CHECK_LOOSE;
        case Scope.VARARGS_COMPATIBLE:
            return CHECK_VARARG;
        default:
            return CHECK_STRICT;
        }
    }

    /**
     * 18.5.3 Functional Interface Parameterization Inference
     */
    public ReferenceBinding inferFunctionalInterfaceParameterization(LambdaExpression lambda, BlockScope blockScope,
            ParameterizedTypeBinding targetTypeWithWildCards) {
        TypeBinding[] q = createBoundsForFunctionalInterfaceParameterizationInference(targetTypeWithWildCards);
        if (q == null || q.length != lambda.arguments().length) {
            // fail  TODO: can this still happen here?
        } else {
            if (reduceWithEqualityConstraints(lambda.argumentTypes(), q)) {
                ReferenceBinding genericType = targetTypeWithWildCards.genericType();
                TypeBinding[] a = targetTypeWithWildCards.arguments; // a is not-null by construction of parameterizedWithWildcard()
                TypeBinding[] aprime = getFunctionInterfaceArgumentSolutions(a);
                // If F<A'1, ..., A'm> is a well-formed type, ...
                ParameterizedTypeBinding ptb = blockScope.environment().createParameterizedType(genericType, aprime,
                        targetTypeWithWildCards.enclosingType());
                TypeVariableBinding[] vars = ptb.genericType().typeVariables();
                ParameterizedTypeBinding captured = ptb.capture(blockScope, lambda.sourceStart, lambda.sourceEnd);
                for (int i = 0; i < vars.length; i++) {
                    if (vars[i].boundCheck(captured, aprime[i], blockScope, lambda) == BoundCheckStatus.MISMATCH)
                        return null;
                }
                return ptb;
            }
        }
        return targetTypeWithWildCards;
    }

    /**
     * Create initial bound set for 18.5.3 Functional Interface Parameterization Inference
     * @param functionalInterface the functional interface F<A1,..Am>
     * @return the parameter types Q1..Qk of the function type of the type F<1, ..., m>, or null 
     */
    TypeBinding[] createBoundsForFunctionalInterfaceParameterizationInference(
            ParameterizedTypeBinding functionalInterface) {
        if (this.currentBounds == null)
            this.currentBounds = new BoundSet();
        TypeBinding[] a = functionalInterface.arguments;
        if (a == null)
            return null;
        InferenceVariable[] alpha = addInitialTypeVariableSubstitutions(a);

        for (int i = 0; i < a.length; i++) {
            TypeBound bound;
            if (a[i].kind() == Binding.WILDCARD_TYPE) {
                WildcardBinding wildcard = (WildcardBinding) a[i];
                switch (wildcard.boundKind) {
                case Wildcard.EXTENDS:
                    bound = new TypeBound(alpha[i], wildcard.allBounds(), ReductionResult.SUBTYPE);
                    break;
                case Wildcard.SUPER:
                    bound = new TypeBound(alpha[i], wildcard.bound, ReductionResult.SUPERTYPE);
                    break;
                case Wildcard.UNBOUND:
                    bound = new TypeBound(alpha[i], this.object, ReductionResult.SUBTYPE);
                    break;
                default:
                    continue; // cannot
                }
            } else {
                bound = new TypeBound(alpha[i], a[i], ReductionResult.SAME);
            }
            this.currentBounds.addBound(bound, this.environment);
        }
        TypeBinding falpha = substitute(functionalInterface);
        return falpha.getSingleAbstractMethod(this.scope, true).parameters;
    }

    public boolean reduceWithEqualityConstraints(TypeBinding[] p, TypeBinding[] q) {
        if (p != null) {
            for (int i = 0; i < p.length; i++) {
                try {
                    if (!this.reduceAndIncorporate(ConstraintTypeFormula.create(p[i], q[i], ReductionResult.SAME)))
                        return false;
                } catch (InferenceFailureException e) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 18.5.4 More Specific Method Inference
     */
    public boolean isMoreSpecificThan(MethodBinding m1, MethodBinding m2, boolean isVarArgs, boolean isVarArgs2) {
        // TODO: we don't yet distinguish vararg-with-passthrough from vararg-with-exactly-one-vararg-arg
        if (isVarArgs != isVarArgs2) {
            return isVarArgs2;
        }
        Expression[] arguments = this.invocationArguments;
        int numInvocArgs = arguments == null ? 0 : arguments.length;
        TypeVariableBinding[] p = m2.typeVariables();
        TypeBinding[] s = m1.parameters;
        TypeBinding[] t = new TypeBinding[m2.parameters.length];
        createInitialBoundSet(p);
        for (int i = 0; i < t.length; i++)
            t[i] = substitute(m2.parameters[i]);

        try {
            for (int i = 0; i < numInvocArgs; i++) {
                TypeBinding si = getParameter(s, i, isVarArgs);
                TypeBinding ti = getParameter(t, i, isVarArgs);
                Boolean result = moreSpecificMain(si, ti, this.invocationArguments[i]);
                if (result == Boolean.FALSE)
                    return false;
                if (result == null)
                    if (!reduceAndIncorporate(ConstraintTypeFormula.create(si, ti, ReductionResult.SUBTYPE)))
                        return false;
            }
            if (t.length == numInvocArgs + 1) {
                TypeBinding skplus1 = getParameter(s, numInvocArgs, true);
                TypeBinding tkplus1 = getParameter(t, numInvocArgs, true);
                if (!reduceAndIncorporate(ConstraintTypeFormula.create(skplus1, tkplus1, ReductionResult.SUBTYPE)))
                    return false;
            }
            return solve() != null;
        } catch (InferenceFailureException e) {
            return false;
        }
    }

    // FALSE: inference fails
    // TRUE:  constraints have been incorporated
    // null:  need to create the si <: ti constraint
    private Boolean moreSpecificMain(TypeBinding si, TypeBinding ti, Expression expri)
            throws InferenceFailureException {
        if (si.isProperType(true) && ti.isProperType(true)) {
            return expri.sIsMoreSpecific(si, ti, this.scope) ? Boolean.TRUE : Boolean.FALSE;
        }
        // "if Ti is not a functional interface type" specifically requests the si <: ti constraint created by our caller
        if (!ti.isFunctionalInterface(this.scope))
            return null;

        TypeBinding funcI = ti.original();
        // "It must be determined whether Si satisfies the following five conditions:"
        // (we negate each condition for early exit):
        if (si.isFunctionalInterface(this.scope)) { // bullet 1
            if (siSuperI(si, funcI) || siSubI(si, funcI))
                return null; // bullets 2 & 3
            if (si instanceof IntersectionTypeBinding18) {
                TypeBinding[] elements = ((IntersectionTypeBinding18) si).intersectingTypes;
                checkSuper: {
                    for (int i = 0; i < elements.length; i++)
                        if (!siSuperI(elements[i], funcI))
                            break checkSuper;
                    return null; // bullet 4 
                    // each element of the intersection is a superinterface of I, or a parameterization of a superinterface of I.
                }
                for (int i = 0; i < elements.length; i++)
                    if (siSubI(elements[i], funcI))
                        return null; // bullet 5
                // some element of the intersection is a subinterface of I, or a parameterization of a subinterface of I.   
            }
            // all passed, time to do some work:
            TypeBinding siCapture = si.capture(this.scope, expri.sourceStart, expri.sourceEnd);
            MethodBinding sam = siCapture.getSingleAbstractMethod(this.scope, false); // no wildcards should be left needing replacement
            TypeBinding[] u = sam.parameters;
            TypeBinding r1 = sam.isConstructor() ? sam.declaringClass : sam.returnType;
            sam = ti.getSingleAbstractMethod(this.scope, true); // TODO
            TypeBinding[] v = sam.parameters;
            TypeBinding r2 = sam.isConstructor() ? sam.declaringClass : sam.returnType;
            return Boolean.valueOf(checkExpression(expri, u, r1, v, r2));
        }
        return null;
    }

    private boolean checkExpression(Expression expri, TypeBinding[] u, TypeBinding r1, TypeBinding[] v,
            TypeBinding r2) throws InferenceFailureException {
        if (expri instanceof LambdaExpression && !((LambdaExpression) expri).argumentsTypeElided()) {
            for (int i = 0; i < u.length; i++) {
                if (!reduceAndIncorporate(ConstraintTypeFormula.create(u[i], v[i], ReductionResult.SAME)))
                    return false;
            }
            if (r2.id == TypeIds.T_void)
                return true;
            LambdaExpression lambda = (LambdaExpression) expri;
            Expression[] results = lambda.resultExpressions();
            if (results != Expression.NO_EXPRESSIONS) {
                if (r1.isFunctionalInterface(this.scope) && r2.isFunctionalInterface(this.scope)
                        && !(r1.isCompatibleWith(r2) || r2.isCompatibleWith(r1))) {
                    // "these rules are applied recursively to R1 and R2, for each result expression in expi."
                    // (what does "applied .. to R1 and R2" mean? Why mention R1/R2 and not U/V?)
                    for (int i = 0; i < results.length; i++) {
                        if (!checkExpression(results[i], u, r1, v, r2))
                            return false;
                    }
                    return true;
                }
                checkPrimitive1: if (r1.isPrimitiveType() && !r2.isPrimitiveType()) {
                    // check: each result expression is a standalone expression of a primitive type
                    for (int i = 0; i < results.length; i++) {
                        if (results[i].isPolyExpression()
                                || (results[i].resolvedType != null && !results[i].resolvedType.isPrimitiveType()))
                            break checkPrimitive1;
                    }
                    return true;
                }
                checkPrimitive2: if (r2.isPrimitiveType() && !r1.isPrimitiveType()) {
                    for (int i = 0; i < results.length; i++) {
                        // for all expressions (not for any expression not)
                        if (!((!results[i].isPolyExpression()
                                && (results[i].resolvedType != null && !results[i].resolvedType.isPrimitiveType())) // standalone of a referencetype
                                || results[i].isPolyExpression())) // or a poly
                            break checkPrimitive2;
                    }
                    return true;
                }
            }
            return reduceAndIncorporate(ConstraintTypeFormula.create(r1, r2, ReductionResult.SUBTYPE));
        } else if (expri instanceof ReferenceExpression && ((ReferenceExpression) expri).isExactMethodReference()) {
            ReferenceExpression reference = (ReferenceExpression) expri;
            for (int i = 0; i < u.length; i++) {
                if (!reduceAndIncorporate(ConstraintTypeFormula.create(u[i], v[i], ReductionResult.SAME)))
                    return false;
            }
            if (r2.id == TypeIds.T_void)
                return true;
            MethodBinding method = reference.getExactMethod();
            TypeBinding returnType = method.isConstructor() ? method.declaringClass : method.returnType;
            if (r1.isPrimitiveType() && !r2.isPrimitiveType() && returnType.isPrimitiveType())
                return true;
            if (r2.isPrimitiveType() && !r1.isPrimitiveType() && !returnType.isPrimitiveType())
                return true;
            return reduceAndIncorporate(ConstraintTypeFormula.create(r1, r2, ReductionResult.SUBTYPE));
        } else if (expri instanceof ConditionalExpression) {
            ConditionalExpression cond = (ConditionalExpression) expri;
            return checkExpression(cond.valueIfTrue, u, r1, v, r2)
                    && checkExpression(cond.valueIfFalse, u, r1, v, r2);
        } else if (expri instanceof SwitchExpression) {
            SwitchExpression se = (SwitchExpression) expri;
            for (Expression re : se.resultExpressions) {
                if (!checkExpression(re, u, r1, v, r2))
                    return false;
            }
            return true;
        } else {
            return false;
        }
    }

    private boolean siSuperI(TypeBinding si, TypeBinding funcI) {
        if (TypeBinding.equalsEquals(si, funcI) || TypeBinding.equalsEquals(si.original(), funcI))
            return true;
        TypeBinding[] superIfcs = funcI.superInterfaces();
        if (superIfcs == null)
            return false;
        for (int i = 0; i < superIfcs.length; i++) {
            if (siSuperI(si, superIfcs[i].original()))
                return true;
        }
        return false;
    }

    private boolean siSubI(TypeBinding si, TypeBinding funcI) {
        if (TypeBinding.equalsEquals(si, funcI) || TypeBinding.equalsEquals(si.original(), funcI))
            return true;
        TypeBinding[] superIfcs = si.superInterfaces();
        if (superIfcs == null)
            return false;
        for (int i = 0; i < superIfcs.length; i++) {
            if (siSubI(superIfcs[i], funcI))
                return true;
        }
        return false;
    }

    // ========== Below this point: implementation of the generic algorithm: ==========

    /**
     * Try to solve the inference problem defined by constraints and bounds previously registered.
     * @return a bound set representing the solution, or null if inference failed
     * @throws InferenceFailureException a compile error has been detected during inference
     */
    public /*@Nullable*/ BoundSet solve(boolean inferringApplicability) throws InferenceFailureException {

        if (!reduce())
            return null;
        if (!this.currentBounds.incorporate(this))
            return null;
        if (inferringApplicability)
            this.b2 = this.currentBounds.copy(); // Preserve the result after reduction, without effects of resolve() for later use in invocation type inference.

        BoundSet solution = resolve(this.inferenceVariables);

        /* If inferring applicability make a final pass over the initial constraints preserved as final constraints to make sure they hold true at a macroscopic level.
           See https://bugs.eclipse.org/bugs/show_bug.cgi?id=426537#c55 onwards.
        */
        if (inferringApplicability && solution != null && this.finalConstraints != null) {
            for (ConstraintExpressionFormula constraint : this.finalConstraints) {
                if (constraint.left.isPolyExpression())
                    continue; // avoid redundant re-inference, inner poly's own constraints get validated in its own context & poly invocation type inference proved compatibility against target. 
                constraint.applySubstitution(solution, this.inferenceVariables);
                if (!this.currentBounds.reduceOneConstraint(this, constraint)) {
                    return null;
                }
            }
        }
        return solution;
    }

    public /*@Nullable*/ BoundSet solve() throws InferenceFailureException {
        return solve(false);
    }

    public /*@Nullable*/ BoundSet solve(InferenceVariable[] toResolve) throws InferenceFailureException {
        if (!reduce())
            return null;
        if (!this.currentBounds.incorporate(this))
            return null;

        return resolve(toResolve);
    }

    /**
     * JLS 18.2. reduce all initial constraints 
     * @throws InferenceFailureException 
     */
    private boolean reduce() throws InferenceFailureException {
        // Caution: This can be reentered recursively even as an earlier call is munching through the constraints !
        for (int i = 0; this.initialConstraints != null && i < this.initialConstraints.length; i++) {
            final ConstraintFormula currentConstraint = this.initialConstraints[i];
            if (currentConstraint == null)
                continue;
            this.initialConstraints[i] = null;
            if (!this.currentBounds.reduceOneConstraint(this, currentConstraint))
                return false;
        }
        this.initialConstraints = null;
        return true;
    }

    /**
     * Have all inference variables been instantiated successfully?
     */
    public boolean isResolved(BoundSet boundSet) {
        if (this.inferenceVariables != null) {
            for (int i = 0; i < this.inferenceVariables.length; i++) {
                if (!boundSet.isInstantiated(this.inferenceVariables[i]))
                    return false;
            }
        }
        return true;
    }

    /**
     * Retrieve the resolved solutions for all given type variables.
     * @param typeParameters
     * @param boundSet where instantiations are to be found
     * @return array containing the substituted types or <code>null</code> elements for any type variable that could not be substituted.
     */
    public TypeBinding /*@Nullable*/[] getSolutions(TypeVariableBinding[] typeParameters, InvocationSite site,
            BoundSet boundSet) {
        int len = typeParameters.length;
        TypeBinding[] substitutions = new TypeBinding[len];
        InferenceVariable[] outerVariables = null;
        if (this.outerContext != null && this.outerContext.stepCompleted < TYPE_INFERRED)
            outerVariables = this.outerContext.inferenceVariables;
        for (int i = 0; i < typeParameters.length; i++) {
            for (int j = 0; j < this.inferenceVariables.length; j++) {
                InferenceVariable variable = this.inferenceVariables[j];
                if (isSameSite(variable.site, site)
                        && TypeBinding.equalsEquals(variable.typeParameter, typeParameters[i])) {
                    TypeBinding outerVar = null;
                    if (outerVariables != null
                            && (outerVar = boundSet.getEquivalentOuterVariable(variable, outerVariables)) != null)
                        substitutions[i] = outerVar;
                    else
                        substitutions[i] = boundSet.getInstantiation(variable, this.environment);
                    break;
                }
            }
            if (substitutions[i] == null)
                return null;
        }
        return substitutions;
    }

    /** When inference produces a new constraint, reduce it to a suitable type bound and add the latter to the bound set. */
    public boolean reduceAndIncorporate(ConstraintFormula constraint) throws InferenceFailureException {
        return this.currentBounds.reduceOneConstraint(this, constraint); // TODO(SH): should we immediately call a diat incorporate, or can we simply wait for the next round?
    }

    /**
     * <b>JLS 18.4</b> Resolution
     * @return answer null if some constraint resolved to FALSE, otherwise the boundset representing the solution
     * @throws InferenceFailureException 
     */
    private /*@Nullable*/ BoundSet resolve(InferenceVariable[] toResolve) throws InferenceFailureException {
        this.captureId = 0;
        // NOTE: 18.5.2 ... 
        // "(While it was necessary to demonstrate that the inference variables in B1 could be resolved
        //   in order to establish applicability, the resulting instantiations are not considered part of B1.)
        // For this reason, resolve works on a temporary bound set, copied before any modification.
        BoundSet tmpBoundSet = this.currentBounds;
        if (this.inferenceVariables != null) {
            // find a minimal set of dependent variables:
            Set<InferenceVariable> variableSet;
            while ((variableSet = getSmallestVariableSet(tmpBoundSet, toResolve)) != null) {
                int oldNumUninstantiated = tmpBoundSet.numUninstantiatedVariables(this.inferenceVariables);
                final int numVars = variableSet.size();
                if (numVars > 0) {
                    final InferenceVariable[] variables = variableSet.toArray(new InferenceVariable[numVars]);
                    variables: if (!tmpBoundSet.hasCaptureBound(variableSet)) {
                        // try to instantiate this set of variables in a fresh copy of the bound set:
                        BoundSet prevBoundSet = tmpBoundSet;
                        tmpBoundSet = tmpBoundSet.copy();
                        for (int j = 0; j < variables.length; j++) {
                            InferenceVariable variable = variables[j];
                            // try lower bounds:
                            TypeBinding[] lowerBounds = tmpBoundSet.lowerBounds(variable, true/*onlyProper*/);
                            if (lowerBounds != Binding.NO_TYPES) {
                                TypeBinding lub = this.scope.lowerUpperBound(lowerBounds);
                                if (lub == TypeBinding.VOID || lub == null)
                                    return null;
                                tmpBoundSet.addBound(new TypeBound(variable, lub, ReductionResult.SAME),
                                        this.environment);
                            } else {
                                TypeBinding[] upperBounds = tmpBoundSet.upperBounds(variable, true/*onlyProper*/);
                                // check exception bounds:
                                if (tmpBoundSet.inThrows.contains(variable.prototype())
                                        && tmpBoundSet.hasOnlyTrivialExceptionBounds(variable, upperBounds)) {
                                    TypeBinding runtimeException = this.scope
                                            .getType(TypeConstants.JAVA_LANG_RUNTIMEEXCEPTION, 3);
                                    tmpBoundSet.addBound(
                                            new TypeBound(variable, runtimeException, ReductionResult.SAME),
                                            this.environment);
                                } else {
                                    // try upper bounds:
                                    TypeBinding glb = this.object;
                                    if (upperBounds != Binding.NO_TYPES) {
                                        if (upperBounds.length == 1) {
                                            glb = upperBounds[0];
                                        } else {
                                            TypeBinding[] glbs = Scope.greaterLowerBound(upperBounds, this.scope,
                                                    this.environment);
                                            if (glbs == null) {
                                                return null;
                                            } else if (glbs.length == 1) {
                                                glb = glbs[0];
                                            } else {
                                                glb = intersectionFromGlb(glbs);
                                                if (glb == null) {
                                                    // inconsistent intersection
                                                    tmpBoundSet = prevBoundSet; // clean up
                                                    break variables; // and start over                                       
                                                }
                                            }
                                        }
                                    }
                                    tmpBoundSet.addBound(new TypeBound(variable, glb, ReductionResult.SAME),
                                            this.environment);
                                }
                            }
                        }
                        if (tmpBoundSet.incorporate(this))
                            continue;
                        tmpBoundSet = prevBoundSet;// clean-up for second attempt
                    }
                    // Otherwise, a second attempt is made...
                    Sorting.sortInferenceVariables(variables); // ensure stability of capture IDs
                    final CaptureBinding18[] zs = new CaptureBinding18[numVars];
                    for (int j = 0; j < numVars; j++)
                        zs[j] = freshCapture(variables[j]);
                    final BoundSet kurrentBoundSet = tmpBoundSet;
                    Substitution theta = new Substitution() {
                        @Override
                        public LookupEnvironment environment() {
                            return InferenceContext18.this.environment;
                        }

                        @Override
                        public boolean isRawSubstitution() {
                            return false;
                        }

                        @Override
                        public TypeBinding substitute(TypeVariableBinding typeVariable) {
                            for (int j = 0; j < numVars; j++)
                                if (TypeBinding.equalsEquals(variables[j], typeVariable))
                                    return zs[j];
                            /* If we have an instantiation, lower it to the instantiation. We don't want downstream abstractions to be confused about multiple versions of bounds without
                               and with instantiations propagated by incorporation. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=430686. There is no value whatsoever in continuing
                               to speak in two tongues. Also fixes https://bugs.eclipse.org/bugs/show_bug.cgi?id=425031.
                            */
                            if (typeVariable instanceof InferenceVariable) {
                                InferenceVariable inferenceVariable = (InferenceVariable) typeVariable;
                                TypeBinding instantiation = kurrentBoundSet.getInstantiation(inferenceVariable,
                                        null);
                                if (instantiation != null)
                                    return instantiation;
                            }
                            return typeVariable;
                        }
                    };
                    for (int j = 0; j < numVars; j++) {
                        InferenceVariable variable = variables[j];
                        CaptureBinding18 zsj = zs[j];
                        // add lower bounds:
                        TypeBinding[] lowerBounds = tmpBoundSet.lowerBounds(variable, true/*onlyProper*/);
                        if (lowerBounds != Binding.NO_TYPES) {
                            TypeBinding lub = this.scope.lowerUpperBound(lowerBounds);
                            if (lub != TypeBinding.VOID && lub != null)
                                zsj.lowerBound = lub;
                        }
                        // add upper bounds:
                        TypeBinding[] upperBounds = tmpBoundSet.upperBounds(variable, false/*onlyProper*/);
                        if (upperBounds != Binding.NO_TYPES) {
                            for (int k = 0; k < upperBounds.length; k++)
                                upperBounds[k] = Scope.substitute(theta, upperBounds[k]);
                            if (!setUpperBounds(zsj, upperBounds))
                                continue; // at violation of well-formedness skip this candidate and proceed
                        }
                        if (tmpBoundSet == this.currentBounds)
                            tmpBoundSet = tmpBoundSet.copy();
                        Iterator<ParameterizedTypeBinding> captureKeys = tmpBoundSet.captures.keySet().iterator();
                        Set<ParameterizedTypeBinding> toRemove = new HashSet<ParameterizedTypeBinding>();
                        while (captureKeys.hasNext()) {
                            ParameterizedTypeBinding key = captureKeys.next();
                            int len = key.arguments.length;
                            for (int i = 0; i < len; i++) {
                                if (TypeBinding.equalsEquals(key.arguments[i], variable)) {
                                    toRemove.add(key);
                                    break;
                                }
                            }
                        }
                        captureKeys = toRemove.iterator();
                        while (captureKeys.hasNext())
                            tmpBoundSet.captures.remove(captureKeys.next());
                        tmpBoundSet.addBound(new TypeBound(variable, zsj, ReductionResult.SAME), this.environment);
                    }
                    if (tmpBoundSet.incorporate(this)) {
                        if (tmpBoundSet.numUninstantiatedVariables(this.inferenceVariables) == oldNumUninstantiated)
                            return null; // abort because we made no progress
                        continue;
                    }
                    return null;
                }
            }
        }
        return tmpBoundSet;
    }

    private TypeBinding intersectionFromGlb(TypeBinding[] glbs) {
        ReferenceBinding[] refGlbs = new ReferenceBinding[glbs.length];
        for (int i = 0; i < glbs.length; i++) {
            TypeBinding typeBinding = glbs[i];
            if (typeBinding instanceof ReferenceBinding) {
                refGlbs[i] = (ReferenceBinding) typeBinding;
            } else {
                return null;
            }
        }
        IntersectionTypeBinding18 intersection = (IntersectionTypeBinding18) this.environment
                .createIntersectionType18(refGlbs);
        if (ReferenceBinding.isConsistentIntersection(intersection.intersectingTypes))
            return intersection;
        return null;
    }

    int captureId = 0;

    /** For 18.4: "Let Z1, ..., Zn be fresh type variables" use capture bindings. */
    private CaptureBinding18 freshCapture(InferenceVariable variable) {
        int id = this.captureId++;
        char[] sourceName = CharOperation.concat("Z".toCharArray(), '#', String.valueOf(id).toCharArray(), '-', //$NON-NLS-1$
                variable.sourceName);
        int start = this.currentInvocation != null ? this.currentInvocation.sourceStart() : 0;
        int end = this.currentInvocation != null ? this.currentInvocation.sourceEnd() : 0;
        return new CaptureBinding18(this.scope.enclosingSourceType(), sourceName,
                variable.typeParameter.shortReadableName(), start, end, id, this.environment);
    }
    // === ===

    private boolean setUpperBounds(CaptureBinding18 typeVariable, TypeBinding[] substitutedUpperBounds) {
        // 18.4: ... define the upper bound of Zi as glb(L1, ..., Lk)
        if (substitutedUpperBounds.length == 1) {
            return typeVariable.setUpperBounds(substitutedUpperBounds, this.object); // shortcut
        } else {
            TypeBinding[] glbs = Scope.greaterLowerBound(substitutedUpperBounds, this.scope, this.environment);
            if (glbs == null)
                return false;
            if (typeVariable.lowerBound != null) {
                for (int i = 0; i < glbs.length; i++) {
                    if (!typeVariable.lowerBound.isCompatibleWith(glbs[i]))
                        return false; // not well-formed
                }
            }
            // for deterministic results sort this array by id:
            sortTypes(glbs);
            if (!typeVariable.setUpperBounds(glbs, this.object))
                return false;
        }
        return true;
    }

    static void sortTypes(TypeBinding[] types) {
        Arrays.sort(types, new Comparator<TypeBinding>() {
            @Override
            public int compare(TypeBinding o1, TypeBinding o2) {
                int i1 = o1.id, i2 = o2.id;
                return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1));
            }
        });
    }

    /**
     * Find the smallest set of uninstantiated inference variables not depending
     * on any uninstantiated variable outside the set.
     */
    private Set<InferenceVariable> getSmallestVariableSet(BoundSet bounds, InferenceVariable[] subSet) {
        // "Given a set of inference variables to resolve, let V be the union of this set and
        //  all variables upon which the resolution of at least one variable in this set depends." 
        Set<InferenceVariable> v = new HashSet<InferenceVariable>();
        Map<InferenceVariable, Set<InferenceVariable>> dependencies = new HashMap<>(); // compute only once, store for the final loop over 'v'.
        for (InferenceVariable iv : subSet) {
            Set<InferenceVariable> tmp = new HashSet<>();
            addDependencies(bounds, tmp, iv);
            dependencies.put(iv, tmp);
            v.addAll(tmp);
        }
        // "If every variable in V has an instantiation, then resolution succeeds and this procedure terminates."
        //  -> (implicit if result remains unassigned)
        // "Otherwise, let { 1, ..., n } be a non-empty subset of uninstantiated variables in V such that ...
        int min = Integer.MAX_VALUE;
        Set<InferenceVariable> result = null;
        // "i) for all i (1  i  n), ..."
        for (InferenceVariable currentVariable : v) {
            if (!bounds.isInstantiated(currentVariable)) {
                // "... if i depends on the resolution of a variable , then either  has an instantiation or there is some j such that  = j; ..."
                Set<InferenceVariable> set = dependencies.get(currentVariable);
                if (set == null) // not an element of the original subSet, still need to fetch this var's dependencies
                    addDependencies(bounds, set = new HashSet<>(), currentVariable);
                //  "... and ii) there exists no non-empty proper subset of { 1, ..., n } with this property."
                int cur = set.size();
                if (cur == 1)
                    return set; // won't get smaller
                if (cur < min) {
                    result = set;
                    min = cur;
                }
            }
        }
        return result;
    }

    private void addDependencies(BoundSet boundSet, Set<InferenceVariable> variableSet,
            InferenceVariable currentVariable) {
        if (boundSet.isInstantiated(currentVariable))
            return; // not added
        if (!variableSet.add(currentVariable))
            return; // already present
        for (int j = 0; j < this.inferenceVariables.length; j++) {
            InferenceVariable nextVariable = this.inferenceVariables[j];
            if (TypeBinding.equalsEquals(nextVariable, currentVariable))
                continue;
            if (boundSet.dependsOnResolutionOf(currentVariable, nextVariable))
                addDependencies(boundSet, variableSet, nextVariable);
        }
    }

    private ConstraintFormula pickFromCycle(Set<ConstraintFormula> c) {
        // Detail from 18.5.2 bullet 6.1

        // Note on performance: this implementation could quite possibly be optimized a lot.
        // However, we only *very rarely* reach here,
        // so nobody should really be affected by the performance penalty paid here.

        // Note on spec conformance: the spec seems to require _all_ criteria (i)-(iv) to be fulfilled
        // with the sole exception of (iii), which should only be used, if _any_ constraints matching (i) & (ii)
        // also fulfill this condition.
        // Experiments, however, show that strict application of the above is prone to failing to pick any constraint,
        // causing non-termination of the algorithm.
        // Since that is not acceptable, I'm *interpreting* the spec to request a search for a constraint
        // that "best matches" the given conditions.

        // collect all constraints participating in a cycle
        HashMap<ConstraintFormula, Set<ConstraintFormula>> dependencies = new HashMap<ConstraintFormula, Set<ConstraintFormula>>();
        Set<ConstraintFormula> cycles = new HashSet<ConstraintFormula>();
        for (ConstraintFormula constraint : c) {
            Collection<InferenceVariable> infVars = constraint.inputVariables(this);
            for (ConstraintFormula other : c) {
                if (other == constraint)
                    continue;
                if (dependsOn(infVars, other.outputVariables(this))) {
                    // found a dependency, record it:
                    Set<ConstraintFormula> targetSet = dependencies.get(constraint);
                    if (targetSet == null)
                        dependencies.put(constraint, targetSet = new HashSet<ConstraintFormula>());
                    targetSet.add(other);
                    // look for a cycle:
                    Set<ConstraintFormula> nodesInCycle = new HashSet<ConstraintFormula>();
                    if (isReachable(dependencies, other, constraint, new HashSet<ConstraintFormula>(),
                            nodesInCycle)) {
                        // found a cycle, record the involved nodes:
                        cycles.addAll(nodesInCycle);
                    }
                }
            }
        }
        Set<ConstraintFormula> outside = new HashSet<ConstraintFormula>(c);
        outside.removeAll(cycles);

        Set<ConstraintFormula> candidatesII = new HashSet<ConstraintFormula>();
        // (i): participates in a cycle:
        candidates: for (ConstraintFormula candidate : cycles) {
            Collection<InferenceVariable> infVars = candidate.inputVariables(this);
            // (ii) does not depend on any constraints outside the cycle
            for (ConstraintFormula out : outside) {
                if (dependsOn(infVars, out.outputVariables(this)))
                    continue candidates;
            }
            candidatesII.add(candidate);
        }
        if (candidatesII.isEmpty())
            candidatesII = c; // not spec'ed but needed to avoid returning null below, witness: java.util.stream.Collectors

        // tentatively: (iii)  has the form Expression  T
        Set<ConstraintFormula> candidatesIII = new HashSet<ConstraintFormula>();
        for (ConstraintFormula candidate : candidatesII) {
            if (candidate instanceof ConstraintExpressionFormula)
                candidatesIII.add(candidate);
        }
        if (candidatesIII.isEmpty()) {
            candidatesIII = candidatesII; // no constraint fulfills (iii) -> ignore this condition
        } else { // candidatesIII contains all relevant constraints Expression  T
            // (iv) contains an expression that appears to the left of the expression
            //       of every other constraint satisfying the previous three requirements

            // collect containment info regarding all expressions in candidate constraints:
            // (a) find minimal enclosing expressions:
            Map<ConstraintExpressionFormula, ConstraintExpressionFormula> expressionContainedBy = new HashMap<ConstraintExpressionFormula, ConstraintExpressionFormula>();
            for (ConstraintFormula one : candidatesIII) {
                ConstraintExpressionFormula oneCEF = (ConstraintExpressionFormula) one;
                Expression exprOne = oneCEF.left;
                for (ConstraintFormula two : candidatesIII) {
                    if (one == two)
                        continue;
                    ConstraintExpressionFormula twoCEF = (ConstraintExpressionFormula) two;
                    Expression exprTwo = twoCEF.left;
                    if (doesExpressionContain(exprOne, exprTwo)) {
                        ConstraintExpressionFormula previous = expressionContainedBy.get(two);
                        if (previous == null || doesExpressionContain(previous.left, exprOne)) // only if improving
                            expressionContainedBy.put(twoCEF, oneCEF);
                    }
                }
            }
            // (b) build the tree from the above
            Map<ConstraintExpressionFormula, Set<ConstraintExpressionFormula>> containmentForest = new HashMap<ConstraintExpressionFormula, Set<ConstraintExpressionFormula>>();
            for (Map.Entry<ConstraintExpressionFormula, ConstraintExpressionFormula> parentRelation : expressionContainedBy
                    .entrySet()) {
                ConstraintExpressionFormula parent = parentRelation.getValue();
                Set<ConstraintExpressionFormula> children = containmentForest.get(parent);
                if (children == null)
                    containmentForest.put(parent, children = new HashSet<ConstraintExpressionFormula>());
                children.add(parentRelation.getKey());
            }

            // approximate the spec by searching the largest containment tree:
            int bestRank = -1;
            ConstraintExpressionFormula candidate = null;
            for (ConstraintExpressionFormula parent : containmentForest.keySet()) {
                int rank = rankNode(parent, expressionContainedBy, containmentForest);
                if (rank > bestRank) {
                    bestRank = rank;
                    candidate = parent;
                }
            }
            if (candidate != null)
                return candidate;
        }

        if (candidatesIII.isEmpty())
            throw new IllegalStateException("cannot pick constraint from cyclic set"); //$NON-NLS-1$
        return candidatesIII.iterator().next();
    }

    /**
     * Does the first constraint depend on the other?
     * The first constraint is represented by its input variables and the other constraint by its output variables.
     */
    private boolean dependsOn(Collection<InferenceVariable> inputsOfFirst,
            Collection<InferenceVariable> outputsOfOther) {
        for (InferenceVariable iv : inputsOfFirst) {
            for (InferenceVariable otherIV : outputsOfOther)
                if (this.currentBounds.dependsOnResolutionOf(iv, otherIV))
                    return true;
        }
        return false;
    }

    /** Does 'deps' contain a chain of dependencies leading from 'from' to 'to'? */
    private boolean isReachable(Map<ConstraintFormula, Set<ConstraintFormula>> deps, ConstraintFormula from,
            ConstraintFormula to, Set<ConstraintFormula> nodesVisited, Set<ConstraintFormula> nodesInCycle) {
        if (from == to) {
            nodesInCycle.add(from);
            return true;
        }
        if (!nodesVisited.add(from))
            return false;
        Set<ConstraintFormula> targetSet = deps.get(from);
        if (targetSet != null) {
            for (ConstraintFormula tgt : targetSet) {
                if (isReachable(deps, tgt, to, nodesVisited, nodesInCycle)) {
                    nodesInCycle.add(from);
                    return true;
                }
            }
        }
        return false;
    }

    /** Does exprOne lexically contain exprTwo? */
    private boolean doesExpressionContain(Expression exprOne, Expression exprTwo) {
        if (exprTwo.sourceStart > exprOne.sourceStart) {
            return exprTwo.sourceEnd <= exprOne.sourceEnd;
        } else if (exprTwo.sourceStart == exprOne.sourceStart) {
            return exprTwo.sourceEnd < exprOne.sourceEnd;
        }
        return false;
    }

    /** non-roots answer -1, roots answer the size of the spanned tree */
    private int rankNode(ConstraintExpressionFormula parent,
            Map<ConstraintExpressionFormula, ConstraintExpressionFormula> expressionContainedBy,
            Map<ConstraintExpressionFormula, Set<ConstraintExpressionFormula>> containmentForest) {
        if (expressionContainedBy.get(parent) != null)
            return -1; // not a root
        Set<ConstraintExpressionFormula> children = containmentForest.get(parent);
        if (children == null)
            return 1; // unconnected node or leaf
        int sum = 1;
        for (ConstraintExpressionFormula child : children) {
            int cRank = rankNode(child, expressionContainedBy, containmentForest);
            if (cRank > 0)
                sum += cRank;
        }
        return sum;
    }

    private Set<ConstraintFormula> findBottomSet(Set<ConstraintFormula> constraints,
            Set<InferenceVariable> allOutputVariables, List<Set<InferenceVariable>> components) {
        // 18.5.2 bullet 5.(1)
        //  A subset of constraints is selected, satisfying the property that,
        //  for each constraint, no input variable can influence an output variable of another constraint in C. ...
        //  An inference variable  can influence an inference variable  if  depends on the resolution of  (18.4), or vice versa;
        //  or if there exists a third inference variable  such that  can influence  and  can influence .  ...
        Set<ConstraintFormula> result = new HashSet<ConstraintFormula>();
        constraintLoop: for (ConstraintFormula constraint : constraints) {
            for (InferenceVariable in : constraint.inputVariables(this)) {
                if (canInfluenceAnyOf(in, allOutputVariables, components))
                    continue constraintLoop;
            }
            result.add(constraint);
        }
        return result;
    }

    private boolean canInfluenceAnyOf(InferenceVariable in, Set<InferenceVariable> allOuts,
            List<Set<InferenceVariable>> components) {
        // can influence == lives in the same component
        for (Set<InferenceVariable> component : components) {
            if (component.contains(in)) {
                for (InferenceVariable out : allOuts)
                    if (component.contains(out))
                        return true;
                return false;
            }
        }
        return false;
    }

    Set<InferenceVariable> allOutputVariables(Set<ConstraintFormula> constraints) {
        Set<InferenceVariable> result = new HashSet<InferenceVariable>();
        Iterator<ConstraintFormula> it = constraints.iterator();
        while (it.hasNext()) {
            result.addAll(it.next().outputVariables(this));
        }
        return result;
    }

    private TypeBinding[] varArgTypes(TypeBinding[] parameters, int k) {
        TypeBinding[] types = new TypeBinding[k];
        int declaredLength = parameters.length - 1;
        System.arraycopy(parameters, 0, types, 0, declaredLength);
        TypeBinding last = ((ArrayBinding) parameters[declaredLength]).elementsType();
        for (int i = declaredLength; i < k; i++)
            types[i] = last;
        return types;
    }

    public SuspendedInferenceRecord enterPolyInvocation(InvocationSite invocation, Expression[] innerArguments) {
        SuspendedInferenceRecord record = new SuspendedInferenceRecord(this.currentInvocation,
                this.invocationArguments, this.inferenceVariables, this.inferenceKind,
                this.usesUncheckedConversion);
        this.inferenceVariables = null;
        this.invocationArguments = innerArguments;
        this.currentInvocation = invocation;
        this.usesUncheckedConversion = false;
        return record;
    }

    public SuspendedInferenceRecord enterLambda(LambdaExpression lambda) {
        SuspendedInferenceRecord record = new SuspendedInferenceRecord(this.currentInvocation,
                this.invocationArguments, this.inferenceVariables, this.inferenceKind,
                this.usesUncheckedConversion);
        this.inferenceVariables = null;
        this.invocationArguments = null;
        this.usesUncheckedConversion = false;
        return record;
    }

    public void integrateInnerInferenceB2(InferenceContext18 innerCtx) {
        this.currentBounds.addBounds(innerCtx.b2, this.environment);
        this.inferenceVariables = innerCtx.inferenceVariables;
        this.inferenceKind = innerCtx.inferenceKind;
        if (!isSameSite(innerCtx.currentInvocation, this.currentInvocation))
            innerCtx.outerContext = this;
        this.usesUncheckedConversion = innerCtx.usesUncheckedConversion;
    }

    public void resumeSuspendedInference(SuspendedInferenceRecord record, InferenceContext18 innerContext) {
        // merge inference variables:
        boolean firstTime = collectInnerContext(innerContext);
        if (this.inferenceVariables == null) { // no new ones, assume we aborted prematurely
            this.inferenceVariables = record.inferenceVariables;
        } else if (!firstTime) {
            // Use a set to eliminate duplicates.
            final Set<InferenceVariable> uniqueVariables = new LinkedHashSet<>();
            uniqueVariables.addAll(Arrays.asList(record.inferenceVariables));
            uniqueVariables.addAll(Arrays.asList(this.inferenceVariables));
            this.inferenceVariables = uniqueVariables.toArray(new InferenceVariable[uniqueVariables.size()]);
        } else {
            int l1 = this.inferenceVariables.length;
            int l2 = record.inferenceVariables.length;
            System.arraycopy(this.inferenceVariables, 0, this.inferenceVariables = new InferenceVariable[l1 + l2],
                    l2, l1);
            System.arraycopy(record.inferenceVariables, 0, this.inferenceVariables, 0, l2);
        }

        // replace invocation site & arguments:
        this.currentInvocation = record.site;
        this.invocationArguments = record.invocationArguments;
        this.inferenceKind = record.inferenceKind;
        this.usesUncheckedConversion = record.usesUncheckedConversion;
    }

    private boolean collectInnerContext(final InferenceContext18 innerContext) {
        if (innerContext == null) {
            return false;
        }
        if (this.seenInnerContexts == null) {
            this.seenInnerContexts = new HashSet<>();
        }
        return this.seenInnerContexts.add(innerContext);
    }

    private Substitution getResultSubstitution(final BoundSet result) {
        return new Substitution() {
            @Override
            public LookupEnvironment environment() {
                return InferenceContext18.this.environment;
            }

            @Override
            public boolean isRawSubstitution() {
                return false;
            }

            @Override
            public TypeBinding substitute(TypeVariableBinding typeVariable) {
                if (typeVariable instanceof InferenceVariable) {
                    TypeBinding instantiation = result.getInstantiation((InferenceVariable) typeVariable,
                            InferenceContext18.this.environment);
                    if (instantiation != null)
                        return instantiation;
                }
                return typeVariable;
            }
        };
    }

    public boolean isVarArgs() {
        return this.inferenceKind == CHECK_VARARG;
    }

    /**
     * Retrieve the rank'th parameter, possibly respecting varargs invocation, see 15.12.2.4.
     * Returns null if out of bounds and CHECK_VARARG was not requested.
     * Precondition: isVarArgs implies method.isVarargs()
     */
    public static TypeBinding getParameter(TypeBinding[] parameters, int rank, boolean isVarArgs) {
        if (isVarArgs) {
            if (rank >= parameters.length - 1)
                return ((ArrayBinding) parameters[parameters.length - 1]).elementsType();
        } else if (rank >= parameters.length) {
            return null;
        }
        return parameters[rank];
    }

    /**
     * Create a problem method signaling failure of invocation type inference,
     * unless the given candidate is tolerable to be compatible with buggy javac.
     */
    public MethodBinding getReturnProblemMethodIfNeeded(TypeBinding expectedType, MethodBinding method) {
        if (InferenceContext18.SIMULATE_BUG_JDK_8026527 && expectedType != null
                && !(method.original() instanceof SyntheticFactoryMethodBinding)
                && (method.returnType instanceof ReferenceBinding || method.returnType instanceof ArrayBinding)) {
            if (!expectedType.isProperType(true))
                return null; // not ready
            if (this.environment.convertToRawType(method.returnType.erasure(), false)
                    .isCompatibleWith(expectedType))
                return method; // don't count as problem.
        }
        /* We used to check if expected type is null and if so return method, but that is wrong - it injects an incompatible method into overload resolution.
           if we get here with expected type set to null at all, the target context does not define a target type (vanilla context), so inference has done its
           best and nothing more to do than to signal error. 
         */
        ProblemMethodBinding problemMethod = new ProblemMethodBinding(method, method.selector, method.parameters,
                ProblemReasons.InvocationTypeInferenceFailure);
        problemMethod.returnType = expectedType != null ? expectedType : method.returnType;
        problemMethod.inferenceContext = this;
        return problemMethod;
    }

    // debugging:
    @Override
    public String toString() {
        StringBuffer buf = new StringBuffer("Inference Context"); //$NON-NLS-1$
        switch (this.stepCompleted) {
        case NOT_INFERRED:
            buf.append(" (initial)"); //$NON-NLS-1$
            break;
        case APPLICABILITY_INFERRED:
            buf.append(" (applicability inferred)"); //$NON-NLS-1$
            break;
        case TYPE_INFERRED:
            buf.append(" (type inferred)"); //$NON-NLS-1$
            break;
        case TYPE_INFERRED_FINAL:
            buf.append(" (type inferred final)"); //$NON-NLS-1$
            break;
        }
        switch (this.inferenceKind) {
        case CHECK_STRICT:
            buf.append(" (strict)"); //$NON-NLS-1$
            break;
        case CHECK_LOOSE:
            buf.append(" (loose)"); //$NON-NLS-1$
            break;
        case CHECK_VARARG:
            buf.append(" (vararg)"); //$NON-NLS-1$
            break;
        }
        if (this.currentBounds != null && isResolved(this.currentBounds))
            buf.append(" (resolved)"); //$NON-NLS-1$
        buf.append('\n');
        if (this.inferenceVariables != null) {
            buf.append("Inference Variables:\n"); //$NON-NLS-1$
            for (int i = 0; i < this.inferenceVariables.length; i++) {
                buf.append('\t').append(this.inferenceVariables[i].sourceName).append("\t:\t"); //$NON-NLS-1$
                if (this.currentBounds != null && this.currentBounds.isInstantiated(this.inferenceVariables[i]))
                    buf.append(this.currentBounds.getInstantiation(this.inferenceVariables[i], this.environment)
                            .readableName());
                else
                    buf.append("NOT INSTANTIATED"); //$NON-NLS-1$
                buf.append('\n');
            }
        }
        if (this.initialConstraints != null) {
            buf.append("Initial Constraints:\n"); //$NON-NLS-1$
            for (int i = 0; i < this.initialConstraints.length; i++)
                if (this.initialConstraints[i] != null)
                    buf.append('\t').append(this.initialConstraints[i].toString()).append('\n');
        }
        if (this.currentBounds != null)
            buf.append(this.currentBounds.toString());
        return buf.toString();
    }

    /**
     * If 'type' is a parameterized type and one of its arguments is a wildcard answer the casted type, else null.
     * A nonnull answer is ensured to also have nonnull arguments.
     */
    public static ParameterizedTypeBinding parameterizedWithWildcard(TypeBinding type) {
        if (type == null || type.kind() != Binding.PARAMETERIZED_TYPE)
            return null;
        ParameterizedTypeBinding parameterizedType = (ParameterizedTypeBinding) type;
        TypeBinding[] arguments = parameterizedType.arguments;
        if (arguments != null) {
            for (int i = 0; i < arguments.length; i++)
                if (arguments[i].isWildcard())
                    return parameterizedType;
        }
        return null;
    }

    public TypeBinding[] getFunctionInterfaceArgumentSolutions(TypeBinding[] a) {
        int m = a.length;
        TypeBinding[] aprime = new TypeBinding[m];
        for (int i = 0; i < this.inferenceVariables.length; i++) {
            InferenceVariable alphai = this.inferenceVariables[i];
            TypeBinding t = this.currentBounds.getInstantiation(alphai, this.environment);
            if (t != null)
                aprime[i] = t;
            else
                aprime[i] = a[i];
        }
        return aprime;
    }

    /** Record the fact that the given constraint requires unchecked conversion. */
    public void recordUncheckedConversion(ConstraintTypeFormula constraint) {
        if (this.constraintsWithUncheckedConversion == null)
            this.constraintsWithUncheckedConversion = new ArrayList<ConstraintFormula>();
        this.constraintsWithUncheckedConversion.add(constraint);
        this.usesUncheckedConversion = true;
    }

    void reportUncheckedConversions(BoundSet solution) {
        if (this.constraintsWithUncheckedConversion != null) {
            int len = this.constraintsWithUncheckedConversion.size();
            Substitution substitution = getResultSubstitution(solution);
            for (int i = 0; i < len; i++) {
                ConstraintTypeFormula constraint = (ConstraintTypeFormula) this.constraintsWithUncheckedConversion
                        .get(i);
                TypeBinding expectedType = constraint.right;
                TypeBinding providedType = constraint.left;
                if (!expectedType.isProperType(true)) {
                    expectedType = Scope.substitute(substitution, expectedType);
                }
                if (!providedType.isProperType(true)) {
                    providedType = Scope.substitute(substitution, providedType);
                }
                /* FIXME(stephan): enable once we solved:
                (a) avoid duplication with traditional reporting
                (b) improve location to report against
                            if (this.currentInvocation instanceof Expression)
                               this.scope.problemReporter().unsafeTypeConversion((Expression) this.currentInvocation, providedType, expectedType);
                 */
            }
        }
    }

    /** For use by 15.12.2.6 Method Invocation Type */
    public boolean usesUncheckedConversion() {
        return this.constraintsWithUncheckedConversion != null;
    }

    // INTERIM: infrastructure for detecting failures caused by specific known incompleteness:
    public static void missingImplementation(String msg) {
        throw new UnsupportedOperationException(msg);
    }

    public void forwardResults(BoundSet result, Invocation invocation, ParameterizedMethodBinding pmb,
            TypeBinding targetType) {
        if (targetType != null)
            invocation.registerResult(targetType, pmb);
        Expression[] arguments = invocation.arguments();
        for (int i = 0, length = arguments == null ? 0 : arguments.length; i < length; i++) {
            Expression[] expressions = arguments[i].getPolyExpressions();
            for (int j = 0, jLength = expressions.length; j < jLength; j++) {
                Expression expression = expressions[j];
                if (!(expression instanceof Invocation))
                    continue;
                Invocation polyInvocation = (Invocation) expression;
                MethodBinding binding = polyInvocation.binding();
                if (binding == null || !binding.isValidBinding())
                    continue;
                ParameterizedMethodBinding methodSubstitute = null;
                if (binding instanceof ParameterizedGenericMethodBinding) {
                    MethodBinding shallowOriginal = binding.shallowOriginal();
                    TypeBinding[] solutions = getSolutions(shallowOriginal.typeVariables(), polyInvocation, result);
                    if (solutions == null) // in CEF.reduce, we lift inner poly expressions into outer context only if their target type has inference variables. 
                        continue;
                    methodSubstitute = this.environment.createParameterizedGenericMethod(shallowOriginal,
                            solutions);
                } else {
                    if (!binding.isConstructor() || !(binding instanceof ParameterizedMethodBinding))
                        continue; // throw ISE ?
                    MethodBinding shallowOriginal = binding.shallowOriginal();
                    ReferenceBinding genericType = shallowOriginal.declaringClass;
                    TypeBinding[] solutions = getSolutions(genericType.typeVariables(), polyInvocation, result);
                    if (solutions == null) // in CEF.reduce, we lift inner poly expressions into outer context only if their target type has inference variables. 
                        continue;
                    ParameterizedTypeBinding parameterizedType = this.environment.createParameterizedType(
                            genericType, solutions, binding.declaringClass.enclosingType());
                    for (MethodBinding parameterizedMethod : parameterizedType.methods()) {
                        if (parameterizedMethod.original() == shallowOriginal) {
                            methodSubstitute = (ParameterizedMethodBinding) parameterizedMethod;
                            break;
                        }
                    }
                }
                if (methodSubstitute == null || !methodSubstitute.isValidBinding())
                    continue;
                boolean variableArity = pmb.isVarargs();
                final TypeBinding[] parameters = pmb.parameters;
                if (variableArity && parameters.length == arguments.length && i == length - 1) {
                    TypeBinding returnType = methodSubstitute.returnType.capture(this.scope, expression.sourceStart,
                            expression.sourceEnd);
                    if (returnType.isCompatibleWith(parameters[parameters.length - 1], this.scope)) {
                        variableArity = false;
                    }
                }
                TypeBinding parameterType = InferenceContext18.getParameter(parameters, i, variableArity);
                forwardResults(result, polyInvocation, methodSubstitute, parameterType);
            }
        }
    }

    public void cleanUp() {
        this.b2 = null;
        this.currentBounds = null;
    }
}