From fd0099977192f0bbf25295c1b5d46e8969159e48 Mon Sep 17 00:00:00 2001 From: Alexander Udalov Date: Tue, 7 May 2019 15:04:00 +0200 Subject: [PATCH] Render contracts in kotlinp, fix minor bug in kotlinx-metadata-jvm --- libraries/kotlinx-metadata/jvm/ChangeLog.md | 1 + .../src/kotlinx/metadata/impl/writers.kt | 5 +- .../org/jetbrains/kotlin/kotlinp/printers.kt | 135 ++++++++++++++++++ .../kotlinp/test/KotlinpTestGenerated.java | 5 + libraries/tools/kotlinp/testData/Contracts.kt | 66 +++++++++ .../tools/kotlinp/testData/Contracts.txt | 71 +++++++++ 6 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 libraries/tools/kotlinp/testData/Contracts.kt create mode 100644 libraries/tools/kotlinp/testData/Contracts.txt diff --git a/libraries/kotlinx-metadata/jvm/ChangeLog.md b/libraries/kotlinx-metadata/jvm/ChangeLog.md index f3d9fe38972..2219052984f 100644 --- a/libraries/kotlinx-metadata/jvm/ChangeLog.md +++ b/libraries/kotlinx-metadata/jvm/ChangeLog.md @@ -5,6 +5,7 @@ - [`KT-31308`](https://youtrack.jetbrains.com/issue/KT-31308) Add module name extensions to kotlinx-metadata-jvm - [`KT-31338`](https://youtrack.jetbrains.com/issue/KT-31338) Retain "is moved from interface companion" property flag in kotlinx-metadata-jvm - Breaking change: JvmPropertyExtensionVisitor.visit has a new parameter `jvmFlags: Flags` +- Correctly write "null" constant value in effect expression of a contract - Rename `desc` parameters to `signature` in JvmFunctionExtensionVisitor, JvmPropertyExtensionVisitor, JvmConstructorExtensionVisitor ## 0.0.5 diff --git a/libraries/kotlinx-metadata/src/kotlinx/metadata/impl/writers.kt b/libraries/kotlinx-metadata/src/kotlinx/metadata/impl/writers.kt index e665ffa4b44..495ff8e9b91 100644 --- a/libraries/kotlinx-metadata/src/kotlinx/metadata/impl/writers.kt +++ b/libraries/kotlinx-metadata/src/kotlinx/metadata/impl/writers.kt @@ -384,11 +384,10 @@ private fun writeEffectExpression(c: WriteContext, output: (ProtoBuf.Expression. } override fun visitConstantValue(value: Any?) { - @Suppress("UNUSED_VARIABLE") // force exhaustive when - val unused = when (value) { + when (value) { true -> t.constantValue = ProtoBuf.Expression.ConstantValue.TRUE false -> t.constantValue = ProtoBuf.Expression.ConstantValue.FALSE - null -> null + null -> t.constantValue = ProtoBuf.Expression.ConstantValue.NULL else -> throw IllegalArgumentException("Only true, false or null constant values are allowed for effects (was=$value)") } } diff --git a/libraries/tools/kotlinp/src/org/jetbrains/kotlin/kotlinp/printers.kt b/libraries/tools/kotlinp/src/org/jetbrains/kotlin/kotlinp/printers.kt index c892b4da619..e4444537a13 100644 --- a/libraries/tools/kotlinp/src/org/jetbrains/kotlin/kotlinp/printers.kt +++ b/libraries/tools/kotlinp/src/org/jetbrains/kotlin/kotlinp/printers.kt @@ -21,6 +21,7 @@ private fun visitFunction(settings: KotlinpSettings, sb: StringBuilder, flags: F val versionRequirements = mutableListOf() var jvmSignature: JvmMemberSignature? = null var lambdaClassOriginName: String? = null + var contract: String? = null override fun visitReceiverParameterType(flags: Flags): KmTypeVisitor? = printType(flags) { receiverParameterType = it } @@ -39,6 +40,9 @@ private fun visitFunction(settings: KotlinpSettings, sb: StringBuilder, flags: F override fun visitVersionRequirement(): KmVersionRequirementVisitor? = printVersionRequirement { versionRequirements.add(it) } + override fun visitContract(): KmContractVisitor? = + printContract { contract = it } + override fun visitExtensions(type: KmExtensionType): KmFunctionExtensionVisitor? { if (type != JvmFunctionExtensionVisitor.TYPE) return null return object : JvmFunctionExtensionVisitor() { @@ -79,6 +83,9 @@ private fun visitFunction(settings: KotlinpSettings, sb: StringBuilder, flags: F sb.append(": ").append(returnType) } sb.appendln() + if (contract != null) { + sb.appendln(" $contract") + } } } @@ -548,6 +555,134 @@ private fun StringBuilder.appendDeclarationContainerExtensions( } } +private fun printContract(output: (String) -> Unit): KmContractVisitor = + object : KmContractVisitor() { + val effects = mutableListOf() + + override fun visitEffect(type: KmEffectType, invocationKind: KmEffectInvocationKind?): KmEffectVisitor = + printEffect(type, invocationKind) { effects.add(it) } + + override fun visitEnd() { + output(buildString { + appendln("contract {") + for (effect in effects) { + appendln(" $effect") + } + append(" }") + }) + } + } + +private fun printEffect(type: KmEffectType, invocationKind: KmEffectInvocationKind?, output: (String) -> Unit): KmEffectVisitor = + object : KmEffectVisitor() { + var argument: String? = null + var conclusion: String? = null + + override fun visitConstructorArgument(): KmEffectExpressionVisitor = + printEffectExpression { + // If there are several arguments, only the first is taken, see ContractDeserializerImpl.deserializeSimpleEffect + if (argument == null) { + argument = it + } + } + + override fun visitConclusionOfConditionalEffect(): KmEffectExpressionVisitor = + printEffectExpression { conclusion = it } + + override fun visitEnd() { + output(buildString { + when (type) { + KmEffectType.RETURNS_CONSTANT -> { + append("returns(") + if (argument != null) { + append(argument) + } + append(")") + } + KmEffectType.CALLS -> { + append("callsInPlace($argument") + if (invocationKind != null) { + append(", InvocationKind.${invocationKind.name}") + } + append(")") + } + KmEffectType.RETURNS_NOT_NULL -> { + append("returnsNotNull()") + } + } + if (conclusion != null) { + append(" implies ($conclusion)") + } + }) + } + } + +private fun printEffectExpression(output: (String) -> Unit): KmEffectExpressionVisitor = + object : KmEffectExpressionVisitor() { + var flags: Flags = 0 + var parameterIndex: Int? = null + var constantValue: List? = null // Single-element list + var isInstanceType: String? = null + var andArguments = mutableListOf() + var orArguments = mutableListOf() + + override fun visit(flags: Flags, parameterIndex: Int?) { + this.flags = flags + this.parameterIndex = parameterIndex + } + + override fun visitConstantValue(value: Any?) { + constantValue = listOf(value) + } + + override fun visitIsInstanceType(flags: Flags): KmTypeVisitor = + printType(flags) { isInstanceType = it } + + override fun visitAndArgument(): KmEffectExpressionVisitor = + printEffectExpression { andArguments.add(it) } + + override fun visitOrArgument(): KmEffectExpressionVisitor = + printEffectExpression { orArguments.add(it) } + + override fun visitEnd() { + output(buildString { + append( + when { + constantValue != null -> constantValue!!.single().toString() + parameterIndex != null -> "p#$parameterIndex" + else -> "" + } + ) + if (isInstanceType != null) { + append(" ") + if (Flag.EffectExpression.IS_NEGATED(flags)) append("!") + append("is $isInstanceType") + } + if (Flag.EffectExpression.IS_NULL_CHECK_PREDICATE(flags)) { + append(if (Flag.EffectExpression.IS_NEGATED(flags)) " != " else " == ") + append("null") + } + + if (orArguments.isEmpty()) { + for (andArgument in andArguments) { + if (!isEmpty()) append(" && ") + append(wrapIfNeeded(andArgument)) + } + } + if (andArguments.isEmpty()) { + for (orArgument in orArguments) { + if (!isEmpty()) append(" || ") + append(wrapIfNeeded(orArgument)) + } + } + }) + } + + private fun wrapIfNeeded(s: String): String = + // A simple heuristic to avoid wrapping into unnecessary parentheses + if ('&' in s || '|' in s) "($s)" else s + } + interface AbstractPrinter { fun print(klass: T): String } diff --git a/libraries/tools/kotlinp/test/org/jetbrains/kotlin/kotlinp/test/KotlinpTestGenerated.java b/libraries/tools/kotlinp/test/org/jetbrains/kotlin/kotlinp/test/KotlinpTestGenerated.java index 947b66bd710..13021b44036 100644 --- a/libraries/tools/kotlinp/test/org/jetbrains/kotlin/kotlinp/test/KotlinpTestGenerated.java +++ b/libraries/tools/kotlinp/test/org/jetbrains/kotlin/kotlinp/test/KotlinpTestGenerated.java @@ -34,6 +34,11 @@ public class KotlinpTestGenerated extends AbstractKotlinpTest { runTest("libraries/tools/kotlinp/testData/Annotations.kt"); } + @TestMetadata("Contracts.kt") + public void testContracts() throws Exception { + runTest("libraries/tools/kotlinp/testData/Contracts.kt"); + } + @TestMetadata("Lambda.kt") public void testLambda() throws Exception { runTest("libraries/tools/kotlinp/testData/Lambda.kt"); diff --git a/libraries/tools/kotlinp/testData/Contracts.kt b/libraries/tools/kotlinp/testData/Contracts.kt new file mode 100644 index 00000000000..74514449556 --- /dev/null +++ b/libraries/tools/kotlinp/testData/Contracts.kt @@ -0,0 +1,66 @@ +@file:UseExperimental(ExperimentalContracts::class) + +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.contracts.ExperimentalContracts + +fun returnsTrue(condition: Boolean) { + contract { + returns(true) implies (condition) + } +} + +fun returnsNull(condition: Boolean) { + contract { + returns(null) implies (condition) + } +} + +fun returnsNotNull(condition: Boolean) { + contract { + returnsNotNull() implies (condition) + } +} + +fun Any?.receiverIsNotNull(): Boolean { + contract { + returns(true) implies (this@receiverIsNotNull != null) + } + return this != null +} + +inline fun callsInPlaceAtMostOnce(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } +} + +inline fun callsInPlaceUnknown(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.UNKNOWN) + } +} + +fun conjunction(a: Boolean, b: Boolean, c: Boolean) { + contract { + returns() implies (a && !b && c) + } +} + +fun disjunction(a: Boolean, b: Boolean, c: Boolean) { + contract { + returns() implies (a || !b || c) + } +} + +fun complexBoolean(a: Any?, b: Any?, c: Any?, d: Any?) { + contract { + returns() implies ((a != null && c != null) || (b == null && d != null)) + } +} + +fun negatedIsAndConjunction(a: Any?, b: Boolean, c: Any?) { + contract { + returns() implies (a !is List<*> && b && c == null) + } +} diff --git a/libraries/tools/kotlinp/testData/Contracts.txt b/libraries/tools/kotlinp/testData/Contracts.txt new file mode 100644 index 00000000000..4eb4c5f40e9 --- /dev/null +++ b/libraries/tools/kotlinp/testData/Contracts.txt @@ -0,0 +1,71 @@ +// ContractsKt.class +// ------------------------------------------ +package { + + // signature: callsInPlaceAtMostOnce(Lkotlin/jvm/functions/Function0;)V + public final inline fun callsInPlaceAtMostOnce(block: kotlin/Function0): kotlin/Unit + contract { + callsInPlace(p#1, InvocationKind.AT_MOST_ONCE) + } + + // signature: callsInPlaceUnknown(Lkotlin/jvm/functions/Function0;)V + public final inline fun callsInPlaceUnknown(block: kotlin/Function0): kotlin/Unit + contract { + callsInPlace(p#1) + } + + // signature: complexBoolean(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V + public final fun complexBoolean(a: kotlin/Any?, b: kotlin/Any?, c: kotlin/Any?, d: kotlin/Any?): kotlin/Unit + contract { + returns() implies ((p#1 != null && p#3 != null) || (p#2 == null && p#4 != null)) + } + + // signature: conjunction(ZZZ)V + public final fun conjunction(a: kotlin/Boolean, b: kotlin/Boolean, c: kotlin/Boolean): kotlin/Unit + contract { + returns() implies (p#1 && p#2 && p#3) + } + + // signature: disjunction(ZZZ)V + public final fun disjunction(a: kotlin/Boolean, b: kotlin/Boolean, c: kotlin/Boolean): kotlin/Unit + contract { + returns() implies (p#1 || p#2 || p#3) + } + + // signature: negatedIsAndConjunction(Ljava/lang/Object;ZLjava/lang/Object;)V + public final fun negatedIsAndConjunction(a: kotlin/Any?, b: kotlin/Boolean, c: kotlin/Any?): kotlin/Unit + contract { + returns() implies (p#1 !is kotlin/collections/List<*> && p#2 && p#3 == null) + } + + // signature: returnsNotNull(Z)V + public final fun returnsNotNull(condition: kotlin/Boolean): kotlin/Unit + contract { + returnsNotNull() implies (p#1) + } + + // signature: returnsNull(Z)V + public final fun returnsNull(condition: kotlin/Boolean): kotlin/Unit + contract { + returns(null) implies (p#1) + } + + // signature: returnsTrue(Z)V + public final fun returnsTrue(condition: kotlin/Boolean): kotlin/Unit + contract { + returns(true) implies (p#1) + } + + // signature: receiverIsNotNull(Ljava/lang/Object;)Z + public final fun kotlin/Any?.receiverIsNotNull(): kotlin/Boolean + contract { + returns(true) implies (p#0 != null) + } +} +// META-INF/test-module.kotlin_module +// ------------------------------------------ +module { + package { + ContractsKt + } +}