From 173303104e5ea036c3fcb1401beeb4f4353190b0 Mon Sep 17 00:00:00 2001 From: Andrey Breslav Date: Wed, 7 Aug 2013 15:13:06 +0400 Subject: [PATCH] Type Unifier --- .../jetbrains/jet/lang/types/TypeUnifier.java | 170 +++++++++++++ .../jetbrains/jet/types/TypeUnifierTest.java | 237 ++++++++++++++++++ 2 files changed, 407 insertions(+) create mode 100644 compiler/frontend/src/org/jetbrains/jet/lang/types/TypeUnifier.java create mode 100644 compiler/tests/org/jetbrains/jet/types/TypeUnifierTest.java diff --git a/compiler/frontend/src/org/jetbrains/jet/lang/types/TypeUnifier.java b/compiler/frontend/src/org/jetbrains/jet/lang/types/TypeUnifier.java new file mode 100644 index 00000000000..a90a59de82d --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/jet/lang/types/TypeUnifier.java @@ -0,0 +1,170 @@ +/* + * Copyright 2010-2013 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 org.jetbrains.jet.lang.types; + +import com.google.common.base.Predicate; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class TypeUnifier { + + public interface UnificationResult { + boolean isSuccess(); + + @NotNull + Map getSubstitution(); + } + + /** + * Finds a substitution S that turns {@code projectWithVariables} to {@code knownProjection}. + * + * Example: + * known = List + * withVariables = List + * variables = {X} + * + * result = X -> String + * + * Only types accepted by {@code isVariable} are considered variables. + */ + @NotNull + public static UnificationResult unify( + @NotNull TypeProjection knownProjection, + @NotNull TypeProjection projectWithVariables, + @NotNull Predicate isVariable + ) { + UnificationResultImpl result = new UnificationResultImpl(); + doUnify(knownProjection, projectWithVariables, isVariable, result); + return result; + } + + private static void doUnify( + TypeProjection knownProjection, + TypeProjection projectWithVariables, + Predicate isVariable, + UnificationResultImpl result + ) { + JetType known = knownProjection.getType(); + JetType withVariables = projectWithVariables.getType(); + + // in Foo ~ in X => Foo ~ X + Variance knownProjectionKind = knownProjection.getProjectionKind(); + Variance withVariablesProjectionKind = projectWithVariables.getProjectionKind(); + if (knownProjectionKind == withVariablesProjectionKind && knownProjectionKind != Variance.INVARIANT) { + doUnify(new TypeProjection(known), new TypeProjection(withVariables), isVariable, result); + return; + } + + // Foo? ~ X? => Foo ~ X + if (known.isNullable() && withVariables.isNullable()) { + doUnify( + new TypeProjection(knownProjectionKind, TypeUtils.makeNotNullable(known)), + new TypeProjection(withVariablesProjectionKind, TypeUtils.makeNotNullable(withVariables)), + isVariable, + result + ); + return; + } + + // in Foo ~ out X => fail + // in Foo ~ X => may be OK + if (knownProjectionKind != withVariablesProjectionKind && withVariablesProjectionKind != Variance.INVARIANT) { + result.fail(); + return; + } + + // Foo ~ X? => fail + if (!known.isNullable() && withVariables.isNullable()) { + result.fail(); + return; + } + + // Foo ~ X => x |-> Foo + TypeConstructor maybeVariable = withVariables.getConstructor(); + if (isVariable.apply(maybeVariable)) { + result.put(maybeVariable, new TypeProjection(knownProjectionKind, known)); + return; + } + + // Foo? ~ Foo || in Foo ~ Foo || Foo ~ Bar + boolean structuralMismatch = known.isNullable() != withVariables.isNullable() + || knownProjectionKind != withVariablesProjectionKind + || !known.getConstructor().equals(withVariables.getConstructor()); + if (structuralMismatch) { + result.fail(); + return; + } + + // Foo ~ Foo + if (known.getArguments().size() != withVariables.getArguments().size()) { + result.fail(); + return; + } + + // Foo ~ Foo + if (known.getArguments().isEmpty()) { + return; + } + + // Foo<...> ~ Foo<...> + List knownArguments = known.getArguments(); + List withVariablesArguments = withVariables.getArguments(); + for (int i = 0; i < knownArguments.size(); i++) { + TypeProjection knownArg = knownArguments.get(i); + TypeProjection withVariablesArg = withVariablesArguments.get(i); + + doUnify(knownArg, withVariablesArg, isVariable, result); + } + } + + private static class UnificationResultImpl implements UnificationResult { + private boolean success = true; + private final Map substitution = Maps.newHashMapWithExpectedSize(1); + private final Set failedVariables = Sets.newHashSetWithExpectedSize(0); + + @Override + public boolean isSuccess() { + return success; + } + + public void fail() { + success = false; + } + + @Override + @NotNull + public Map getSubstitution() { + return substitution; + } + + public void put(TypeConstructor key, TypeProjection value) { + if (failedVariables.contains(key)) return; + + TypeProjection oldValue = substitution.put(key, value); + if (oldValue != null && !oldValue.equals(value)) { + substitution.remove(key); + failedVariables.add(key); + fail(); + } + } + } +} diff --git a/compiler/tests/org/jetbrains/jet/types/TypeUnifierTest.java b/compiler/tests/org/jetbrains/jet/types/TypeUnifierTest.java new file mode 100644 index 00000000000..fea28e3f968 --- /dev/null +++ b/compiler/tests/org/jetbrains/jet/types/TypeUnifierTest.java @@ -0,0 +1,237 @@ +/* + * Copyright 2010-2013 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 org.jetbrains.jet.types; + +import com.google.common.base.Predicate; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.jet.ConfigurationKind; +import org.jetbrains.jet.JetLiteFixture; +import org.jetbrains.jet.JetTestUtils; +import org.jetbrains.jet.cli.jvm.compiler.JetCoreEnvironment; +import org.jetbrains.jet.di.InjectorForTests; +import org.jetbrains.jet.lang.descriptors.TypeParameterDescriptor; +import org.jetbrains.jet.lang.descriptors.annotations.AnnotationDescriptor; +import org.jetbrains.jet.lang.descriptors.impl.TypeParameterDescriptorImpl; +import org.jetbrains.jet.lang.psi.JetPsiFactory; +import org.jetbrains.jet.lang.psi.JetTypeProjection; +import org.jetbrains.jet.lang.psi.JetTypeReference; +import org.jetbrains.jet.lang.resolve.TypeResolver; +import org.jetbrains.jet.lang.resolve.name.Name; +import org.jetbrains.jet.lang.resolve.scopes.JetScope; +import org.jetbrains.jet.lang.resolve.scopes.RedeclarationHandler; +import org.jetbrains.jet.lang.resolve.scopes.WritableScope; +import org.jetbrains.jet.lang.resolve.scopes.WritableScopeImpl; +import org.jetbrains.jet.lang.types.*; +import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +public class TypeUnifierTest extends JetLiteFixture { + private Set variables; + + private KotlinBuiltIns builtIns; + private TypeResolver typeResolver; + private TypeParameterDescriptor x; + private TypeParameterDescriptor y; + + @Override + protected JetCoreEnvironment createEnvironment() { + return createEnvironmentWithMockJdk(ConfigurationKind.ALL); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + builtIns = KotlinBuiltIns.getInstance(); + + InjectorForTests injector = new InjectorForTests(getProject(), JetTestUtils.createEmptyModule()); + typeResolver = injector.getTypeResolver(); + x = createTypeVariable("X"); + y = createTypeVariable("Y"); + variables = Sets.newHashSet(x.getTypeConstructor(), y.getTypeConstructor()); + } + + private static TypeParameterDescriptor createTypeVariable(String name) { + return TypeParameterDescriptorImpl.createWithDefaultBound( + KotlinBuiltIns.getInstance().getBuiltInsModule(), Collections.emptyList(), false, Variance.INVARIANT, + Name.identifier(name), 0); + } + + public void testNoVariables() throws Exception { + doTest("Any", "Any", expect()); + doTest("Any", "Int", expect(false)); + doTest("Any?", "Any?", expect()); + doTest("Any?", "Any", expect(false)); + doTest("Any", "Any?", expect(false)); + doTest("List", "List", expect()); + doTest("List", "List", expect(false)); + doTest("List", "List", expect()); + doTest("List", "List", expect(false)); + doTest("List", "List", expect(false)); + doTest("List", "List", expect(false)); + doTest("List", "Set", expect(false)); + doTest("List>", "List>", expect(false)); + doTest("List>", "List>", expect()); + } + + public void testVariables() throws Exception { + doTest("Any", "X", expect("X", "Any")); + + doTest("List", "List", expect("X", "Any")); + doTest("List", "Set", expect(false)); + + doTest("List>", "List", expect("X", "List")); + doTest("List>", "List>", expect("X", "Any")); + doTest("List>", "List>", expect(false)); + + doTest("Map", "Map", expect("X", "Any")); + doTest("Map", "Map", expect("X", "Any", "Y", "String")); + doTest("Map", "Map", expect("X", "Any")); + doTest("Map", "Map", expect(false, "X", "Any")); + + doTest("X", "X", expect("X", "X")); + doTest("List", "List", expect("X", "X")); + } + + public void testDifferentValuesForOneVariable() throws Exception { + doTest("Map", "Map", expect(false)); + } + + public void testVariablesAndNulls() throws Exception { + doTest("Any?", "X?", expect("X", "Any")); + doTest("Any?", "X", expect("X", "Any?")); + doTest("Any", "X?", expect(false)); + + doTest("List", "List", expect("X", "Any")); + doTest("List", "List", expect("X", "Any?")); + doTest("List", "List", expect(false)); + + doTest("List?", "List?", expect("X", "Any")); + doTest("List", "List?", expect(false)); + doTest("List?", "List", expect(false)); + } + + public void testVariablesAndProjections() throws Exception { + doTest("in Any", "X", expect("X", "in Any")); + doTest("in Any", "in X", expect("X", "Any")); + doTest("Any", "out X", expect(false)); + doTest("in Any", "out X", expect(false)); + + doTest("List", "List", expect("X", "in Any")); + doTest("List", "List", expect("X", "out Any")); + doTest("List", "List", expect("X", "Any")); + doTest("List", "List", expect(false)); + doTest("List", "List", expect(false)); + } + + public void testVariablesNullsAndProjections() throws Exception { + doTest("in Any?", "X", expect("X", "in Any?")); + doTest("in Any", "X?", expect(false)); + } + + public void testIllFormedTypes() throws Exception { + doTest("List", "List", expect(false)); + doTest("Map", "Map", expect(false)); + } + + private static Map expect(String... strs) { + return expect(true, strs); + } + + private static Map expect(boolean success, String... strs) { + Map map = Maps.newHashMap(); + putResult(map, success); + for (int i = 0; i < strs.length; i += 2) { + String key = strs[i]; + String value = strs[i + 1]; + map.put(key, value); + } + return map; + } + + private void doTest(String known, String withVariables, @NotNull Map expected) { + TypeUnifier.UnificationResult map = TypeUnifier.unify( + makeType(known), + makeType(withVariables), + new Predicate() { + @Override + public boolean apply(TypeConstructor tc) { + return variables.contains(tc); + } + } + ); + assertEquals(expected, toStrings(map)); + } + + @Nullable + private static Map toStrings(TypeUnifier.UnificationResult map) { + Map result = Maps.newHashMap(); + putResult(result, map.isSuccess()); + for (Map.Entry entry : map.getSubstitution().entrySet()) { + result.put(entry.getKey().toString(), entry.getValue().toString()); + } + return result; + } + + private static void putResult(Map result, boolean success) { + result.put("_RESULT_", String.valueOf(success)); + } + + private TypeProjection makeType(String typeStr) { + return makeTypeProjection(builtIns.getBuiltInsScope(), typeStr); + } + + private TypeProjection makeTypeProjection(JetScope scope, String typeStr) { + WritableScopeImpl withX = + new WritableScopeImpl(scope, scope.getContainingDeclaration(), RedeclarationHandler.DO_NOTHING, "With X"); + withX.addClassifierDescriptor(x); + withX.addClassifierDescriptor(y); + withX.changeLockLevel(WritableScope.LockLevel.READING); + + JetTypeProjection projection = JetPsiFactory.createTypeArguments(getProject(), "<" + typeStr + ">").getArguments().get(0); + + JetTypeReference typeReference = projection.getTypeReference(); + assert typeReference != null; + JetType type = typeResolver.resolveType(withX, typeReference, JetTestUtils.DUMMY_TRACE, true); + + return new TypeProjection(getProjectionKind(typeStr, projection), type); + } + + private static Variance getProjectionKind(String typeStr, JetTypeProjection projection) { + Variance variance; + switch (projection.getProjectionKind()) { + case IN: + variance = Variance.IN_VARIANCE; + break; + case OUT: + variance = Variance.OUT_VARIANCE; + break; + case NONE: + variance = Variance.INVARIANT; + break; + default: + throw new UnsupportedOperationException("Star projections are not supported: " + typeStr); + } + return variance; + } +}