Construct equations for '@Contract(pure=true)' analysis

This commit is contained in:
Ilya Klyuchnikov
2014-10-16 08:51:37 +04:00
parent 2f0a28b212
commit db33965635
9 changed files with 269 additions and 77 deletions
@@ -53,10 +53,11 @@ public class BytecodeAnalysisConverter {
ProgressManager.checkCanceled();
Result<Key, Value> rhs = equation.rhs;
HResult result;
HResult hResult;
if (rhs instanceof Final) {
result = new HFinal(((Final<Key, Value>)rhs).value);
} else {
hResult = new HFinal(((Final<Key, Value>)rhs).value);
}
else {
Pending<Key, Value> pending = (Pending<Key, Value>)rhs;
Set<Product<Key, Value>> sumOrigin = pending.sum;
HComponent[] components = new HComponent[sumOrigin.size()];
@@ -64,17 +65,17 @@ public class BytecodeAnalysisConverter {
for (Product<Key, Value> prod : sumOrigin) {
HKey[] intProd = new HKey[prod.ids.size()];
int idI = 0;
for (Key id : prod.ids) {
intProd[idI] = asmKey(id, md);
for (Key key : prod.ids) {
intProd[idI] = asmKey(key, md);
idI++;
}
HComponent intIdComponent = new HComponent(prod.value, intProd);
components[componentI] = intIdComponent;
componentI++;
}
result = new HPending(components);
hResult = new HPending(components);
}
return new DirectionResultPair(mkDirectionKey(equation.id.direction), result);
return new DirectionResultPair(mkDirectionKey(equation.id.direction), hResult);
}
/**
@@ -274,6 +275,23 @@ public class BytecodeAnalysisConverter {
}
/**
* Converts Direction object to int.
*
* 0 - Out
* 1 - NullableOut
* 2 - Pure
*
* 3 - 0-th NOT_NULL
* 4 - 0-th NULLABLE
* ...
*
* 11 - 1-st NOT_NULL
* 12 - 1-st NULLABLE
*
* @param dir direction of analysis
* @return unique int for direction
*/
static int mkDirectionKey(Direction dir) {
if (dir == Out) {
return 0;
@@ -281,17 +299,28 @@ public class BytecodeAnalysisConverter {
else if (dir == NullableOut) {
return 1;
}
else if (dir == Pure) {
return 2;
}
else if (dir instanceof In) {
In in = (In)dir;
// nullity mask is 0/1
return 2 + 8 * in.paramId() + in.nullityMask;
return 3 + 8 * in.paramId() + in.nullityMask;
}
else {
// valueId is [1-5]
InOut inOut = (InOut)dir;
return 4 + 8 * inOut.paramId() + inOut.valueId();
return 3 + 8 * inOut.paramId() + 2 + inOut.valueId();
}
}
/**
* Converts int to Direction object.
*
* @param directionKey int representation of direction
* @return Direction object
* @see #mkDirectionKey(Direction)
*/
@NotNull
private static Direction extractDirection(int directionKey) {
if (directionKey == 0) {
@@ -300,15 +329,21 @@ public class BytecodeAnalysisConverter {
else if (directionKey == 1) {
return NullableOut;
}
else if (directionKey == 2) {
return Pure;
}
else {
directionKey--;
int paramId = directionKey / 8;
int subDirection = directionKey % 8;
if (subDirection <= 2) {
return new In(paramId, subDirection - 1);
int paramKey = directionKey - 3;
int paramId = paramKey / 8;
// shifting first 3 values - now we have key [0 - 7]
int subDirectionId = paramKey % 8;
// 0 - 1 - @NotNull, @Nullable, parameter
if (subDirectionId <= 1) {
return new In(paramId, subDirectionId);
}
else {
return new InOut(paramId, Value.values()[subDirection - 3]);
int valueId = subDirectionId - 2;
return new InOut(paramId, Value.values()[valueId]);
}
}
}
@@ -88,6 +88,9 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
return ourInternalVersion + (ourEnabled ? 0xFF : 0);
}
/**
* Externalizer for primary method keys.
*/
private static class HKeyDescriptor implements KeyDescriptor<Bytes>, DifferentSerializableBytesImplyNonEqualityPolicy {
@Override
@@ -115,6 +118,9 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
}
}
/**
* Externalizer for compressed equations.
*/
public static class HEquationsExternalizer implements DataExternalizer<HEquations>, DifferentSerializableBytesImplyNonEqualityPolicy {
@Override
public void save(@NotNull DataOutput out, HEquations eqs) throws IOException {
@@ -42,7 +42,6 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
public static final Final<Key, Value> FINAL_BOT = new Final<Key, Value>(Value.Bot);
public static final Final<Key, Value> FINAL_NOT_NULL = new Final<Key, Value>(Value.NotNull);
public static final Final<Key, Value> FINAL_NULL = new Final<Key, Value>(Value.Null);
private static final List<Equation<Key, Value>> EMPTY_EQUATIONS = Collections.EMPTY_LIST;
@NotNull
@Override
@@ -50,17 +49,19 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
HashMap<Bytes, HEquations> map = new HashMap<Bytes, HEquations>();
try {
MessageDigest md = BytecodeAnalysisConverter.getMessageDigest();
Map<Key, List<Equation<Key, Value>>> rawEquations = processClass(new ClassReader(inputData.getContent()), inputData.getFile().getPresentableUrl());
for (Map.Entry<Key, List<Equation<Key, Value>>> entry: rawEquations.entrySet()) {
Key primaryKey = entry.getKey();
Key serKey = new Key(primaryKey.method, primaryKey.direction, true);
Map<Key, List<Equation<Key, Value>>> allEquations = processClass(new ClassReader(inputData.getContent()), inputData.getFile().getPresentableUrl());
for (Map.Entry<Key, List<Equation<Key, Value>>> entry: allEquations.entrySet()) {
List<Equation<Key, Value>> equations = entry.getValue();
List<DirectionResultPair> result = new ArrayList<DirectionResultPair>(equations.size());
for (Equation<Key, Value> equation : equations) {
result.add(BytecodeAnalysisConverter.convert(equation, md));
Key methodKey = entry.getKey();
// method equations with raw (not-compressed keys)
List<Equation<Key, Value>> rawMethodEquations = entry.getValue();
//
List<DirectionResultPair> compressedMethodEquations =
new ArrayList<DirectionResultPair>(rawMethodEquations.size());
for (Equation<Key, Value> equation : rawMethodEquations) {
compressedMethodEquations.add(BytecodeAnalysisConverter.convert(equation, md));
}
map.put(new Bytes(BytecodeAnalysisConverter.asmKey(serKey, md).key), new HEquations(result, primaryKey.stable));
map.put(new Bytes(BytecodeAnalysisConverter.asmKey(methodKey, md).key), new HEquations(compressedMethodEquations, methodKey.stable));
}
}
catch (ProcessCanceledException e) {
@@ -71,7 +72,6 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
// so here we suppose that exception is due to incorrect bytecode
LOG.debug("Unexpected Error during indexing of bytecode", e);
}
//return map;
return map;
}
@@ -113,6 +113,13 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
};
}
/**
* Facade for analysis, it invokes specialized analyses for branching/non-branching methods.
*
* @param methodNode asm node for method
* @param jsr whether a method has jsr instruction
* @return pair of (primaryKey, equations)
*/
private Pair<Key, List<Equation<Key, Value>>> processMethod(final MethodNode methodNode, boolean jsr) {
ProgressManager.checkCanceled();
final Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc);
@@ -125,8 +132,15 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
final boolean stable = stableClass || (methodNode.access & STABLE_FLAGS) != 0 || "<init>".equals(methodNode.name);
Key primaryKey = new Key(method, Out, stable);
// 4*n: for each reference parameter: @NotNull IN, @Nullable, null -> ... contract, !null -> contract
// 3: @NotNull OUT, @Nullable OUT, purity analysis
List<Equation<Key, Value>> equations = new ArrayList<Equation<Key, Value>>(argumentTypes.length * 4 + 3);
equations.add(PurityAnalysis.analyze(method, methodNode, stable));
if (argumentTypes.length == 0 && !isInterestingResult) {
return Pair.create(primaryKey, EMPTY_EQUATIONS);
// no need to continue analysis
return Pair.create(primaryKey, equations);
}
try {
@@ -142,19 +156,18 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
}
}
if (branching) {
RichControlFlow richControlFlow = new RichControlFlow(graph, dfs);
if (richControlFlow.reducible()) {
return Pair.create(primaryKey,
processBranchingMethod(method, methodNode, richControlFlow, argumentTypes, isReferenceResult, isInterestingResult, stable, jsr));
processBranchingMethod(method, methodNode, richControlFlow, argumentTypes, isReferenceResult, isInterestingResult, stable, jsr, equations);
return Pair.create(primaryKey, equations);
}
LOG.debug(method + ": CFG is not reducible");
}
// simple
else {
return Pair.create(primaryKey,
processNonBranchingMethod(method, argumentTypes, graph, isReferenceResult, isBooleanResult, stable));
processNonBranchingMethod(method, argumentTypes, graph, isReferenceResult, isBooleanResult, stable, equations);
return Pair.create(primaryKey, equations);
}
}
return Pair.create(primaryKey, topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable));
@@ -170,17 +183,15 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
}
private List<Equation<Key, Value>> processBranchingMethod(final Method method,
final MethodNode methodNode,
final RichControlFlow richControlFlow,
Type[] argumentTypes,
boolean isReferenceResult,
boolean isInterestingResult,
final boolean stable,
boolean jsr) throws AnalyzerException {
// 4 = @NotNull parameter, @Nullable parameter, null -> ..., !null -> ...
List<Equation<Key, Value>> result = new ArrayList<Equation<Key, Value>>(argumentTypes.length * 4 + 2);
private void processBranchingMethod(final Method method,
final MethodNode methodNode,
final RichControlFlow richControlFlow,
Type[] argumentTypes,
boolean isReferenceResult,
boolean isInterestingResult,
final boolean stable,
boolean jsr,
List<Equation<Key, Value>> result) throws AnalyzerException {
boolean maybeLeakingParameter = isInterestingResult;
for (Type argType : argumentTypes) {
if (ASMUtils.isReferenceType(argType)) {
@@ -218,7 +229,7 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
if (ASMUtils.isReferenceType(argumentTypes[i])) {
boolean possibleNPE = false;
if (leakingParameters[i]) {
NonNullInAnalysis notNullInAnalysis = new NonNullInAnalysis(richControlFlow, new In(i, In.NOT_NULL), stable);
NonNullInAnalysis notNullInAnalysis = new NonNullInAnalysis(richControlFlow, new In(i, In.NOT_NULL_MASK), stable);
Equation<Key, Value> notNullParamEquation = notNullInAnalysis.analyze();
possibleNPE = notNullInAnalysis.possibleNPE;
notNullParam = notNullParamEquation.rhs.equals(FINAL_NOT_NULL);
@@ -226,19 +237,19 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
else {
// parameter is not leaking, so it is definitely NOT @NotNull
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NOT_NULL), stable), FINAL_TOP));
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NOT_NULL_MASK), stable), FINAL_TOP));
}
if (leakingNullableParameters[i]) {
if (notNullParam || possibleNPE) {
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NULLABLE), stable), FINAL_TOP));
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NULLABLE_MASK), stable), FINAL_TOP));
}
else {
result.add(new NullableInAnalysis(richControlFlow, new In(i, In.NULLABLE), stable).analyze());
result.add(new NullableInAnalysis(richControlFlow, new In(i, In.NULLABLE_MASK), stable).analyze());
}
}
else {
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NULLABLE), stable), FINAL_NULL));
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NULLABLE_MASK), stable), FINAL_NULL));
}
if (isInterestingResult) {
@@ -261,17 +272,15 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
}
}
return result;
}
private List<Equation<Key, Value>> processNonBranchingMethod(Method method,
Type[] argumentTypes,
ControlFlowGraph graph,
boolean isReferenceResult,
boolean isBooleanResult,
boolean stable) throws AnalyzerException {
// 4 = @NotNull parameter, @Nullable parameter, null -> ..., !null -> ...
List<Equation<Key, Value>> result = new ArrayList<Equation<Key, Value>>(argumentTypes.length * 4 + 2);
private void processNonBranchingMethod(Method method,
Type[] argumentTypes,
ControlFlowGraph graph,
boolean isReferenceResult,
boolean isBooleanResult,
boolean stable,
List<Equation<Key, Value>> result) throws AnalyzerException {
CombinedAnalysis analyzer = new CombinedAnalysis(method, graph);
analyzer.analyze();
if (isReferenceResult) {
@@ -289,7 +298,6 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
}
}
return result;
}
private List<Equation<Key, Value>> topEquations(Method method,
@@ -305,8 +313,8 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
for (int i = 0; i < argumentTypes.length; i++) {
if (ASMUtils.isReferenceType(argumentTypes[i])) {
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NOT_NULL), stable), FINAL_TOP));
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NULLABLE), stable), FINAL_TOP));
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NOT_NULL_MASK), stable), FINAL_TOP));
result.add(new Equation<Key, Value>(new Key(method, new In(i, In.NULLABLE_MASK), stable), FINAL_TOP));
if (isInterestingResult) {
result.add(new Equation<Key, Value>(new Key(method, new InOut(i, Value.Null), stable), FINAL_TOP));
result.add(new Equation<Key, Value>(new Key(method, new InOut(i, Value.NotNull), stable), FINAL_TOP));
@@ -194,7 +194,7 @@ final class CombinedAnalysis {
}
final Equation<Key, Value> notNullParamEquation(int i, boolean stable) {
final Key key = new Key(method, new In(i, In.NOT_NULL), stable);
final Key key = new Key(method, new In(i, In.NOT_NULL_MASK), stable);
final Result<Key, Value> result;
if (interpreter.dereferencedParams[i]) {
result = new Final<Key, Value>(Value.NotNull);
@@ -207,7 +207,7 @@ final class CombinedAnalysis {
else {
Set<Key> keys = new HashSet<Key>();
for (ParamKey pk: calls) {
keys.add(new Key(pk.method, new In(pk.i, In.NOT_NULL), pk.stable));
keys.add(new Key(pk.method, new In(pk.i, In.NOT_NULL_MASK), pk.stable));
}
result = new Pending<Key, Value>(new SingletonSet<Product<Key, Value>>(new Product<Key, Value>(Value.Top, keys)));
}
@@ -216,7 +216,7 @@ final class CombinedAnalysis {
}
final Equation<Key, Value> nullableParamEquation(int i, boolean stable) {
final Key key = new Key(method, new In(i, In.NULLABLE), stable);
final Key key = new Key(method, new In(i, In.NULLABLE_MASK), stable);
final Result<Key, Value> result;
if (interpreter.dereferencedParams[i] || interpreter.notNullableParams[i] || returnValue instanceof NthParamValue && ((NthParamValue)returnValue).n == i) {
result = new Final<Key, Value>(Value.Top);
@@ -229,7 +229,7 @@ final class CombinedAnalysis {
else {
Set<Product<Key, Value>> sum = new HashSet<Product<Key, Value>>();
for (ParamKey pk: calls) {
sum.add(new Product<Key, Value>(Value.Top, Collections.singleton(new Key(pk.method, new In(pk.i, In.NULLABLE), pk.stable))));
sum.add(new Product<Key, Value>(Value.Top, Collections.singleton(new Key(pk.method, new In(pk.i, In.NULLABLE_MASK), pk.stable))));
}
result = new Pending<Key, Value>(sum);
}
@@ -15,6 +15,8 @@
*/
package com.intellij.codeInspection.bytecodeAnalysis;
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode;
final class Method {
final String internalClassName;
final String methodName;
@@ -36,12 +38,30 @@ final class Method {
return result;
}
/**
* Primary constructor
*
* @param internalClassName class name in asm format
* @param methodName method name
* @param methodDesc method descriptor in asm format
*/
Method(String internalClassName, String methodName, String methodDesc) {
this.internalClassName = internalClassName;
this.methodName = methodName;
this.methodDesc = methodDesc;
}
/**
* Convenient constructor to convert asm instruction into method key
*
* @param mNode asm node from which method key is extracted
*/
Method(MethodInsnNode mNode) {
this.internalClassName = mNode.owner;
this.methodName = mNode.name;
this.methodDesc = mNode.desc;
}
@Override
public String toString() {
return internalClassName + ' ' + methodName + ' ' + methodDesc;
@@ -49,14 +69,19 @@ final class Method {
}
enum Value {
Bot, NotNull, Null, True, False, Top
Bot, NotNull, Null, True, False, Pure, Top
}
interface Direction {
final class In implements Direction {
static final int NOT_NULL = 0;
static final int NULLABLE = 1;
final int paramIndex;
static final int NOT_NULL_MASK = 0;
static final int NULLABLE_MASK = 1;
/**
* @see #NOT_NULL_MASK
* @see #NULLABLE_MASK
*/
final int nullityMask;
In(int paramIndex, int nullityMask) {
@@ -155,6 +180,18 @@ interface Direction {
return -2;
}
};
Direction Pure = new Direction() {
@Override
public int hashCode() {
return -3;
}
@Override
public String toString() {
return "Pure";
}
};
}
final class Key {
@@ -166,7 +166,11 @@ final class HEquation {
return result1;
}
}
class Bytes {
/**
* Bytes of primary HKey of a method.
*/
final class Bytes {
@NotNull
final byte[] bytes;
Bytes(@NotNull byte[] bytes) {
@@ -177,12 +181,7 @@ class Bytes {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Bytes bytes1 = (Bytes)o;
if (!Arrays.equals(bytes, bytes1.bytes)) return false;
return true;
return Arrays.equals(bytes, ((Bytes)o).bytes);
}
@Override
@@ -750,7 +750,7 @@ abstract class NullityInterpreter extends BasicInterpreter {
class NotNullInterpreter extends NullityInterpreter {
NotNullInterpreter() {
super(false, In.NOT_NULL);
super(false, In.NOT_NULL_MASK);
}
@Override
@@ -762,7 +762,7 @@ class NotNullInterpreter extends NullityInterpreter {
class NullableInterpreter extends NullityInterpreter {
NullableInterpreter() {
super(true, In.NULLABLE);
super(true, In.NULLABLE_MASK);
}
@Override
@@ -201,7 +201,7 @@ public class ProjectBytecodeAnalysis {
PsiElement gParent = parent.getParent();
if (gParent instanceof PsiMethod) {
final int index = ((PsiParameterList)parent).getParameterIndex((PsiParameter)owner);
return BytecodeAnalysisConverter.psiKey((PsiMethod)gParent, new In(index, In.NOT_NULL), md);
return BytecodeAnalysisConverter.psiKey((PsiMethod)gParent, new In(index, In.NOT_NULL_MASK), md);
}
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.bytecodeAnalysis;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.org.objectweb.asm.Opcodes;
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode;
import org.jetbrains.org.objectweb.asm.tree.InsnList;
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode;
import org.jetbrains.org.objectweb.asm.tree.MethodNode;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Produces equations for inference of @Contract(pure=true) annotations.
*
* This simple analysis infers @Contract(pure=true) only if the method doesn't have following instructions.
* <ul>
* <li>PUTFIELD</li>
* <li>PUTSTATIC</li>
* <li>IASTORE</li>
* <li>LASTORE</li>
* <li>FASTORE</li>
* <li>DASTORE</li>
* <li>AASTORE</li>
* <li>BASTORE</li>
* <li>CASTORE</li>
* <li>SASTORE</li>
* <li>INVOKEDYNAMIC</li>
* <li>INVOKEINTERFACE</li>
* </ul>
*
* - Nested calls (via INVOKESPECIAL, INVOKESTATIC, INVOKEVIRTUAL) are processed by transitivity.
* @author lambdamix
*/
public class PurityAnalysis {
private static final Result<Key, Value> FINAL_TOP = new Final<Key, Value>(Value.Top);
private static final Result<Key, Value> FINAL_PURE = new Final<Key, Value>(Value.Pure);
static final int UN_ANALYZABLE_FLAG = Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_INTERFACE;
@NotNull
public static Equation<Key, Value> analyze(Method method, MethodNode methodNode, boolean stable) {
Key key = new Key(method, Direction.Pure, stable);
if ((methodNode.access & UN_ANALYZABLE_FLAG) != 0) {
return new Equation<Key, Value>(key, FINAL_TOP);
}
InsnList insns = methodNode.instructions;
// primary keys of invoked methods
Set<Key> invokedKeys = new HashSet<Key>();
for (int i = 0; i < insns.size(); i++) {
AbstractInsnNode insn = insns.get(i);
switch (insn.getOpcode()) {
case Opcodes.PUTFIELD:
case Opcodes.PUTSTATIC:
case Opcodes.IASTORE:
case Opcodes.LASTORE:
case Opcodes.FASTORE:
case Opcodes.DASTORE:
case Opcodes.AASTORE:
case Opcodes.BASTORE:
case Opcodes.CASTORE:
case Opcodes.SASTORE:
case Opcodes.INVOKEDYNAMIC:
case Opcodes.INVOKEINTERFACE:
return new Equation<Key, Value>(key, FINAL_TOP);
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKESTATIC:
invokedKeys.add(new Key(new Method((MethodInsnNode)insn), Direction.Pure, true));
break;
case Opcodes.INVOKEVIRTUAL:
invokedKeys.add(new Key(new Method((MethodInsnNode)insn), Direction.Pure, false));
break;
default:
break;
}
}
if (invokedKeys.isEmpty()) {
return new Equation<Key, Value>(key, FINAL_PURE);
}
else {
HashSet<Product<Key, Value>> sumOfProducts = new HashSet<Product<Key, Value>>();
for (Key call : invokedKeys) {
sumOfProducts.add(new Product<Key, Value>(Value.Top, Collections.singleton(call)));
}
return new Equation<Key, Value>(key, new Pending<Key, Value>(sumOfProducts));
}
}
}