From b0ac046b05f006aaead459efd78677c31b86497f Mon Sep 17 00:00:00 2001 From: Victor Petukhov Date: Fri, 17 Jul 2020 13:36:11 +0300 Subject: [PATCH] NI: decrease fixation priority for variables which have only incorporated from upper bound constraints ^KT-40045 Fixed ^KT-39633 Fixed --- ...irOldFrontendDiagnosticsTestGenerated.java | 10 +++++++ .../components/ConstraintIncorporator.kt | 17 ++++++++--- .../components/ConstraintInjector.kt | 18 ++++++++++-- .../KotlinConstraintSystemCompleter.kt | 28 ++++++------------- .../components/VariableFixationFinder.kt | 17 +++++++---- .../model/ConstraintPositionAndErrors.kt | 5 ++-- .../wrongVariableFixationOrder.kt | 15 ++++++++++ .../wrongVariableFixationOrder.txt | 19 +++++++++++++ .../wrongVariableFixationOrder2.kt | 14 ++++++++++ .../wrongVariableFixationOrder2.txt | 26 +++++++++++++++++ .../checkers/DiagnosticsTestGenerated.java | 10 +++++++ .../DiagnosticsUsingJavacTestGenerated.java | 10 +++++++ 12 files changed, 156 insertions(+), 33 deletions(-) create mode 100644 compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.kt create mode 100644 compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.txt create mode 100644 compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.kt create mode 100644 compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.txt diff --git a/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/FirOldFrontendDiagnosticsTestGenerated.java b/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/FirOldFrontendDiagnosticsTestGenerated.java index cd9326ab3ee..c3a03bf7a80 100644 --- a/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/FirOldFrontendDiagnosticsTestGenerated.java +++ b/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/FirOldFrontendDiagnosticsTestGenerated.java @@ -11036,6 +11036,16 @@ public class FirOldFrontendDiagnosticsTestGenerated extends AbstractFirOldFronte public void testTakingExtensibilityFromDeclarationOfAnonymousFunction() throws Exception { runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/takingExtensibilityFromDeclarationOfAnonymousFunction.kt"); } + + @TestMetadata("wrongVariableFixationOrder.kt") + public void testWrongVariableFixationOrder() throws Exception { + runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.kt"); + } + + @TestMetadata("wrongVariableFixationOrder2.kt") + public void testWrongVariableFixationOrder2() throws Exception { + runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.kt"); + } } } diff --git a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintIncorporator.kt b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintIncorporator.kt index f2a6d3d739e..77f59940bf3 100644 --- a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintIncorporator.kt +++ b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintIncorporator.kt @@ -33,7 +33,8 @@ class ConstraintIncorporator( lowerType: KotlinTypeMarker, upperType: KotlinTypeMarker, shouldTryUseDifferentFlexibilityForUpperType: Boolean, - isFromNullabilityConstraint: Boolean = false + isFromNullabilityConstraint: Boolean = false, + isFromDeclaredUpperBound: Boolean = false ) fun addNewIncorporatedConstraint(typeVariable: TypeVariableMarker, type: KotlinTypeMarker, constraintContext: ConstraintContext) @@ -80,7 +81,15 @@ class ConstraintIncorporator( if (constraint.kind != ConstraintKind.UPPER) { getConstraintsForVariable(typeVariable).forEach { if (it.kind != ConstraintKind.LOWER) { - addNewIncorporatedConstraint(constraint.type, it.type, shouldBeTypeVariableFlexible) + val isFromDeclaredUpperBound = + it.position.from is DeclaredUpperBoundConstraintPosition && it.type.typeConstructor() !is TypeVariableTypeConstructor + + addNewIncorporatedConstraint( + constraint.type, + it.type, + shouldBeTypeVariableFlexible, + isFromDeclaredUpperBound = isFromDeclaredUpperBound + ) } } } @@ -250,7 +259,7 @@ class ConstraintIncorporator( addNewIncorporatedConstraint(targetVariable, newConstraint, constraintContext) } - fun Context.containsConstrainingTypeWithoutProjection( + private fun Context.containsConstrainingTypeWithoutProjection( newConstraint: KotlinTypeMarker, otherConstraint: Constraint ): Boolean { @@ -274,7 +283,7 @@ class ConstraintIncorporator( return otherConstraintCanAddNullabilityToNewOne || newConstraintCanAddNullabilityToOtherOne } - fun Context.getNestedTypeVariables(type: KotlinTypeMarker): List = + private fun Context.getNestedTypeVariables(type: KotlinTypeMarker): List = getNestedArguments(type).mapNotNullTo(SmartList()) { getTypeVariable(it.getType().typeConstructor()) } diff --git a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintInjector.kt b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintInjector.kt index 24901992b3b..674092abba9 100644 --- a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintInjector.kt +++ b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/ConstraintInjector.kt @@ -144,6 +144,8 @@ class ConstraintInjector( private var baseLowerType = position.initialConstraint.a private var baseUpperType = position.initialConstraint.b + private var isIncorporatingConstraintFromDeclaredUpperBound = false + fun extractAllConstraints() = possibleNewConstraints.also { possibleNewConstraints = null } fun addPossibleNewConstraint(variable: TypeVariableMarker, constraint: Constraint) { @@ -252,16 +254,26 @@ class ConstraintInjector( ) } + private fun addNewIncorporatedConstraintFromDeclaredUpperBound(runIsSubtypeOf: Runnable) { + isIncorporatingConstraintFromDeclaredUpperBound = true + runIsSubtypeOf.run() + isIncorporatingConstraintFromDeclaredUpperBound = false + } + // from ConstraintIncorporator.Context override fun addNewIncorporatedConstraint( lowerType: KotlinTypeMarker, upperType: KotlinTypeMarker, shouldTryUseDifferentFlexibilityForUpperType: Boolean, - isFromNullabilityConstraint: Boolean + isFromNullabilityConstraint: Boolean, + isFromDeclaredUpperBound: Boolean ) { if (lowerType === upperType) return if (c.isAllowedType(lowerType) && c.isAllowedType(upperType)) { - runIsSubtypeOf(lowerType, upperType, shouldTryUseDifferentFlexibilityForUpperType, isFromNullabilityConstraint) + fun runIsSubtypeOf() = + runIsSubtypeOf(lowerType, upperType, shouldTryUseDifferentFlexibilityForUpperType, isFromNullabilityConstraint) + + if (isFromDeclaredUpperBound) addNewIncorporatedConstraintFromDeclaredUpperBound(::runIsSubtypeOf) else runIsSubtypeOf() } } @@ -307,6 +319,8 @@ class ConstraintInjector( } } + val position = if (isIncorporatingConstraintFromDeclaredUpperBound) position.copy(isFromDeclaredUpperBound = true) else position + val newConstraint = Constraint( kind, targetType, position, derivedFrom = derivedFrom, diff --git a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/KotlinConstraintSystemCompleter.kt b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/KotlinConstraintSystemCompleter.kt index ab7956d688b..58132260364 100644 --- a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/KotlinConstraintSystemCompleter.kt +++ b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/KotlinConstraintSystemCompleter.kt @@ -279,12 +279,12 @@ class KotlinConstraintSystemCompleter( diagnosticsHolder: KotlinDiagnosticsHolder ): Boolean { while (true) { - val variableForFixation = getVariableReadyForFixation( + val variableForFixation = variableFixationFinder.findFirstVariableForFixation( + this, + getOrderedAllTypeVariables(collectVariablesFromContext, topLevelAtoms), + postponedArguments, completionMode, - topLevelAtoms, - topLevelType, - collectVariablesFromContext, - postponedArguments + topLevelType ) ?: break if (!variableForFixation.hasProperConstraint && completionMode == ConstraintSystemCompletionMode.PARTIAL) @@ -383,27 +383,15 @@ class KotlinConstraintSystemCompleter( return result.toList() } - private fun Context.getVariableReadyForFixation( - completionMode: ConstraintSystemCompletionMode, - topLevelAtoms: List, - topLevelType: UnwrappedType, - collectVariablesFromContext: Boolean, - postponedArguments: List - ) = variableFixationFinder.findFirstVariableForFixation( - this, - getOrderedAllTypeVariables(collectVariablesFromContext, topLevelAtoms), - postponedArguments, - completionMode, - topLevelType - ) - private fun Context.isThereAnyReadyForFixationVariable( completionMode: ConstraintSystemCompletionMode, topLevelAtoms: List, topLevelType: UnwrappedType, collectVariablesFromContext: Boolean, postponedArguments: List - ) = getVariableReadyForFixation(completionMode, topLevelAtoms, topLevelType, collectVariablesFromContext, postponedArguments) != null + ) = variableFixationFinder.findFirstVariableForFixation( + this, getOrderedAllTypeVariables(collectVariablesFromContext, topLevelAtoms), postponedArguments, completionMode, topLevelType + ) != null companion object { fun getOrderedNotAnalyzedPostponedArguments(topLevelAtoms: List): List { diff --git a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/VariableFixationFinder.kt b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/VariableFixationFinder.kt index f9dfc4c6987..efddd7f0db4 100644 --- a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/VariableFixationFinder.kt +++ b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/components/VariableFixationFinder.kt @@ -19,7 +19,6 @@ package org.jetbrains.kotlin.resolve.calls.inference.components import org.jetbrains.kotlin.resolve.calls.inference.components.KotlinConstraintSystemCompleter.ConstraintSystemCompletionMode import org.jetbrains.kotlin.resolve.calls.inference.components.KotlinConstraintSystemCompleter.ConstraintSystemCompletionMode.PARTIAL import org.jetbrains.kotlin.resolve.calls.inference.model.Constraint -import org.jetbrains.kotlin.resolve.calls.inference.model.ConstraintKind import org.jetbrains.kotlin.resolve.calls.inference.model.DeclaredUpperBoundConstraintPosition import org.jetbrains.kotlin.resolve.calls.inference.model.VariableWithConstraints import org.jetbrains.kotlin.resolve.calls.model.PostponedResolvedAtomMarker @@ -57,6 +56,7 @@ class VariableFixationFinder( WITH_COMPLEX_DEPENDENCY, // if type variable T has constraint with non fixed type variable inside (non-top-level): T <: Foo WITH_TRIVIAL_OR_NON_PROPER_CONSTRAINTS, // proper trivial constraint from arguments, Nothing <: T RELATED_TO_ANY_OUTPUT_TYPE, + FROM_INCORPORATION_OF_DECLARED_UPPER_BOUND, READY_FOR_FIXATION, READY_FOR_FIXATION_REIFIED, } @@ -71,6 +71,8 @@ class VariableFixationFinder( hasDependencyToOtherTypeVariables(variable) -> TypeVariableFixationReadiness.WITH_COMPLEX_DEPENDENCY variableHasTrivialOrNonProperConstraints(variable) -> TypeVariableFixationReadiness.WITH_TRIVIAL_OR_NON_PROPER_CONSTRAINTS dependencyProvider.isVariableRelatedToAnyOutputType(variable) -> TypeVariableFixationReadiness.RELATED_TO_ANY_OUTPUT_TYPE + variableHasOnlyIncorporatedConstraintsFromDeclaredUpperBound(variable) -> + TypeVariableFixationReadiness.FROM_INCORPORATION_OF_DECLARED_UPPER_BOUND isReified(variable) -> TypeVariableFixationReadiness.READY_FOR_FIXATION_REIFIED else -> TypeVariableFixationReadiness.READY_FOR_FIXATION } @@ -87,13 +89,19 @@ class VariableFixationFinder( } } - fun Context.variableHasTrivialOrNonProperConstraints(variable: TypeConstructorMarker): Boolean { + private fun Context.variableHasTrivialOrNonProperConstraints(variable: TypeConstructorMarker): Boolean { return notFixedTypeVariables[variable]?.constraints?.all { constraint -> val isProperConstraint = isProperArgumentConstraint(constraint) isProperConstraint && trivialConstraintTypeInferenceOracle.isNotInterestingConstraint(constraint) || !isProperConstraint } ?: false } + private fun Context.variableHasOnlyIncorporatedConstraintsFromDeclaredUpperBound(variable: TypeConstructorMarker): Boolean { + val constraints = notFixedTypeVariables[variable]?.constraints ?: return false + + return constraints.filter { isProperArgumentConstraint(it) }.all { it.position.isFromDeclaredUpperBound } + } + private fun Context.findTypeVariableForFixation( allTypeVariables: List, postponedArguments: List, @@ -106,10 +114,9 @@ class VariableFixationFinder( notFixedTypeVariables, postponedArguments, topLevelType.takeIf { completionMode == PARTIAL }, this ) - val candidate = allTypeVariables.maxBy { getTypeVariableReadiness(it, dependencyProvider) } ?: return null + val candidate = allTypeVariables.maxByOrNull { getTypeVariableReadiness(it, dependencyProvider) } ?: return null - val candidateReadiness = getTypeVariableReadiness(candidate, dependencyProvider) - return when (candidateReadiness) { + return when (getTypeVariableReadiness(candidate, dependencyProvider)) { TypeVariableFixationReadiness.FORBIDDEN -> null TypeVariableFixationReadiness.WITHOUT_PROPER_ARGUMENT_CONSTRAINT -> VariableForFixation(candidate, false) TypeVariableFixationReadiness.WITH_TRIVIAL_OR_NON_PROPER_CONSTRAINTS -> diff --git a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/model/ConstraintPositionAndErrors.kt b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/model/ConstraintPositionAndErrors.kt index 2a2eaa9a6d5..96ad6f5ef23 100644 --- a/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/model/ConstraintPositionAndErrors.kt +++ b/compiler/resolution/src/org/jetbrains/kotlin/resolve/calls/inference/model/ConstraintPositionAndErrors.kt @@ -81,9 +81,10 @@ class DelegatedPropertyConstraintPosition(val topLevelCall: KotlinCall) : Constr override fun toString() = "Constraint from call $topLevelCall for delegated property" } -class IncorporationConstraintPosition( +data class IncorporationConstraintPosition( val from: ConstraintPosition, - val initialConstraint: InitialConstraint + val initialConstraint: InitialConstraint, + var isFromDeclaredUpperBound: Boolean = false ) : ConstraintPosition() { override fun toString() = "Incorporate $initialConstraint from position $from" diff --git a/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.kt b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.kt new file mode 100644 index 00000000000..101911266d0 --- /dev/null +++ b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.kt @@ -0,0 +1,15 @@ +// FIR_IDENTICAL +// !DIAGNOSTICS: -UNUSED_PARAMETER -CAST_NEVER_SUCCEEDS +// Issue: KT-40045 + +interface AAA + +fun assign(dest: R, vararg src: K?): R = null as R + +class DIV(val tabIndex: String): AAA + +fun jsObject(builder: T.() -> Unit): T = null as T + +fun foo(x: DIV) { + assign(x, jsObject { tabIndex }) // tabIndex is resolved in OI and unresolved in NI because T is inferred to Any instead of DIV +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.txt b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.txt new file mode 100644 index 00000000000..799c7449a2d --- /dev/null +++ b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.txt @@ -0,0 +1,19 @@ +package + +public fun assign(/*0*/ dest: R, /*1*/ vararg src: K? /*kotlin.Array*/): R +public fun foo(/*0*/ x: DIV): kotlin.Unit +public fun jsObject(/*0*/ builder: T.() -> kotlin.Unit): T + +public interface AAA { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public final class DIV : AAA { + public constructor DIV(/*0*/ tabIndex: kotlin.String) + public final val tabIndex: kotlin.String + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} diff --git a/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.kt b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.kt new file mode 100644 index 00000000000..70b469d2a02 --- /dev/null +++ b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.kt @@ -0,0 +1,14 @@ +// FIR_IDENTICAL +// !DIAGNOSTICS: -UNUSED_PARAMETER +// Issue: KT-39633 + +interface Proxy + +class A(val left: E) : Proxy + +abstract class Api { + abstract fun magic(): T + inline fun match(proxy: Proxy): A = magic() + inline fun f(x: B): B = g(x) + inline fun g(x: C) = match(A(x)) +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.txt b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.txt new file mode 100644 index 00000000000..324dfb83405 --- /dev/null +++ b/compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.txt @@ -0,0 +1,26 @@ +package + +public final class A : Proxy { + public constructor A(/*0*/ left: E) + public final val left: E + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public abstract class Api { + public constructor Api() + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final inline fun f(/*0*/ x: B): B + public final inline fun g(/*0*/ x: C): C + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public abstract fun magic(): T + public final inline fun match(/*0*/ proxy: Proxy): A + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public interface Proxy { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java index 67c23e42dbf..7e5a914ad9e 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java @@ -11043,6 +11043,16 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTestWithFirVali public void testTakingExtensibilityFromDeclarationOfAnonymousFunction() throws Exception { runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/takingExtensibilityFromDeclarationOfAnonymousFunction.kt"); } + + @TestMetadata("wrongVariableFixationOrder.kt") + public void testWrongVariableFixationOrder() throws Exception { + runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.kt"); + } + + @TestMetadata("wrongVariableFixationOrder2.kt") + public void testWrongVariableFixationOrder2() throws Exception { + runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.kt"); + } } } diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java index 8b6153ac6dc..57586c64594 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java @@ -11038,6 +11038,16 @@ public class DiagnosticsUsingJavacTestGenerated extends AbstractDiagnosticsUsing public void testTakingExtensibilityFromDeclarationOfAnonymousFunction() throws Exception { runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/takingExtensibilityFromDeclarationOfAnonymousFunction.kt"); } + + @TestMetadata("wrongVariableFixationOrder.kt") + public void testWrongVariableFixationOrder() throws Exception { + runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder.kt"); + } + + @TestMetadata("wrongVariableFixationOrder2.kt") + public void testWrongVariableFixationOrder2() throws Exception { + runTest("compiler/testData/diagnostics/tests/inference/completion/postponedArgumentsAnalysis/wrongVariableFixationOrder2.kt"); + } } }