Effects: add (de)serialization of contracts in metadata

- Introduce new definitions in descriptors.proto
- Add new corresponding values in Flags.java
- Introduce ContractSerializer and ContractDeserializer, responsible for
for conversion ContractDescription <-> ProtoBuf.Contract
- Add dependency of 'serialization' module on 'resolution' so that it
could see contracts model.

Note that here we do a lot of seemingly unnecessary hoops, which in fact
necessary to respect existing module system (in particular, to be able
to extract ContractDescription declarations from 'descriptors' module to
make them invisible from reflection)

==========
Effect System introduction: 8/18
This commit is contained in:
Dmitry Savvinov
2017-10-03 15:02:36 +03:00
parent 2c0ef592e6
commit 175d23155e
24 changed files with 8601 additions and 77 deletions
@@ -25,6 +25,7 @@ import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.container.*
import org.jetbrains.kotlin.context.ModuleContext
import org.jetbrains.kotlin.contracts.ContractDeserializerImpl
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.PackagePartProvider
import org.jetbrains.kotlin.frontend.di.configureModule
@@ -109,6 +110,8 @@ fun createContainerForLazyResolveWithJava(
}
targetEnvironment.configure(this)
useImpl<ContractDeserializerImpl>()
}.apply {
get<AbstractJavaClassFinder>().initialize(bindingTrace, get<KotlinCodeAnalyzer>())
}
@@ -0,0 +1,248 @@
/*
* Copyright 2010-2017 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.kotlin.contracts
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.contracts.description.*
import org.jetbrains.kotlin.contracts.description.expressions.*
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.serialization.Flags
import org.jetbrains.kotlin.serialization.ProtoBuf
import org.jetbrains.kotlin.serialization.deserialization.ContractDeserializer
import org.jetbrains.kotlin.serialization.deserialization.DeserializationConfiguration
import org.jetbrains.kotlin.serialization.deserialization.TypeDeserializer
import org.jetbrains.kotlin.serialization.deserialization.TypeTable
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.utils.addIfNotNull
class ContractDeserializerImpl(private val configuration: DeserializationConfiguration) : ContractDeserializer {
override fun deserializeContractFromFunction(
proto: ProtoBuf.Function,
ownerFunction: FunctionDescriptor,
typeTable: TypeTable,
typeDeserializer: TypeDeserializer
): Pair<FunctionDescriptor.UserDataKey<*>, LazyContractProvider>? {
if (!proto.hasContract()) return null
if (!configuration.returnsEffectAllowed && !configuration.callsInPlaceEffectAllowed) return null
val worker = ContractDeserializationWorker(typeTable, typeDeserializer, ownerFunction)
val contract = worker.deserializeContract(proto.contract)
return ContractProviderKey to LazyContractProvider.createInitialized(contract)
}
private class ContractDeserializationWorker(
private val typeTable: TypeTable,
private val typeDeserializer: TypeDeserializer,
private val ownerFunction: FunctionDescriptor
) {
fun deserializeContract(proto: ProtoBuf.Contract): ContractDescription? {
val effects = proto.effectList.map { deserializePossiblyConditionalEffect(it) ?: return null }
return ContractDescription(effects, ownerFunction)
}
private fun deserializePossiblyConditionalEffect(proto: ProtoBuf.Effect): EffectDeclaration? {
if (proto.hasConclusionOfConditionalEffect()) {
// conditional effect
val conclusion = deserializeExpression(proto.conclusionOfConditionalEffect) ?: return null
val effect = deserializeSimpleEffect(proto) ?: return null
return ConditionalEffectDeclaration(effect, conclusion)
}
return deserializeSimpleEffect(proto)
}
private fun deserializeSimpleEffect(proto: ProtoBuf.Effect): EffectDeclaration? {
val type = if (proto.hasEffectType()) proto.effectType else return null
return when (type!!) {
ProtoBuf.Effect.EffectType.RETURNS_CONSTANT -> {
val argument = proto.effectConstructorArgumentList.getOrNull(0)
val returnValue = if (argument == null) ConstantReference.WILDCARD else deserializeExpression(argument) as? ConstantReference ?: return null
ReturnsEffectDeclaration(returnValue)
}
ProtoBuf.Effect.EffectType.RETURNS_NOT_NULL -> {
ReturnsEffectDeclaration(ConstantReference.NOT_NULL)
}
ProtoBuf.Effect.EffectType.CALLS -> {
val argument = proto.effectConstructorArgumentList.getOrNull(0) ?: return null
val callable = extractVariable(argument) ?: return null
val invocationKind = if (proto.hasKind())
proto.kind.toDescriptorInvocationKind() ?: return null
else
InvocationKind.UNKNOWN
CallsEffectDeclaration(callable, invocationKind)
}
}
}
private fun deserializeExpression(proto: ProtoBuf.Expression): BooleanExpression? {
val primitiveType = getPrimitiveType(proto)
val primitiveExpression = extractPrimitiveExpression(proto, primitiveType)
val complexType = getComplexType(proto)
val childs: MutableList<BooleanExpression> = mutableListOf()
childs.addIfNotNull(primitiveExpression)
return when (complexType) {
ComplexExpressionType.AND_SEQUENCE -> {
proto.andArgumentList.mapTo(childs) { deserializeExpression(it) ?: return null }
childs.reduce { acc, booleanExpression -> LogicalAnd(acc, booleanExpression) }
}
ComplexExpressionType.OR_SEQUENCE -> {
proto.orArgumentList.mapTo(childs) { deserializeExpression(it) ?: return null }
childs.reduce { acc, booleanExpression -> LogicalOr(acc, booleanExpression) }
}
null -> primitiveExpression
}
}
private fun extractPrimitiveExpression(proto: ProtoBuf.Expression, primitiveType: PrimitiveExpressionType?): BooleanExpression? {
val isInverted = proto.hasFlags() && Flags.IS_NEGATED.get(proto.flags)
return when (primitiveType) {
PrimitiveExpressionType.VALUE_PARAMETER_REFERENCE, PrimitiveExpressionType.RECEIVER_REFERENCE -> {
(extractVariable(proto) as? BooleanVariableReference?)?.invertIfNecessary(isInverted)
}
PrimitiveExpressionType.CONSTANT ->
(deserializeConstant(proto.constantValue) as? BooleanConstantReference)?.invertIfNecessary(isInverted)
PrimitiveExpressionType.INSTANCE_CHECK -> {
val variable = extractVariable(proto) ?: return null
val type = extractType(proto) ?: return null
IsInstancePredicate(variable, type, isInverted)
}
PrimitiveExpressionType.NULLABILITY_CHECK -> {
val variable = extractVariable(proto) ?: return null
IsNullPredicate(variable, isInverted)
}
null -> null
}
}
private fun BooleanExpression.invertIfNecessary(shouldInvert: Boolean) = if (shouldInvert) LogicalNot(this) else this
private fun extractVariable(proto: ProtoBuf.Expression): VariableReference? {
if (!proto.hasValueParameterReference()) return null
val parameterDescriptor = if (proto.valueParameterReference == 0)
ownerFunction.extensionReceiverParameter ?: return null
else
ownerFunction.valueParameters.getOrNull(proto.valueParameterReference - 1) ?: return null
return if (!KotlinBuiltIns.isBoolean(parameterDescriptor.type))
VariableReference(parameterDescriptor)
else
BooleanVariableReference(parameterDescriptor)
}
private fun ProtoBuf.Effect.InvocationKind.toDescriptorInvocationKind(): InvocationKind? = when (this) {
ProtoBuf.Effect.InvocationKind.AT_MOST_ONCE -> InvocationKind.AT_MOST_ONCE
ProtoBuf.Effect.InvocationKind.EXACTLY_ONCE -> InvocationKind.EXACTLY_ONCE
ProtoBuf.Effect.InvocationKind.AT_LEAST_ONCE -> InvocationKind.AT_LEAST_ONCE
}
private fun extractType(proto: ProtoBuf.Expression): KotlinType? {
val protoType = when {
proto.hasIsInstanceType() -> proto.isInstanceType
proto.hasIsInstanceTypeId() -> typeTable.types.getOrNull(proto.isInstanceTypeId) ?: return null
else -> return null
}
return typeDeserializer.type(protoType)
}
private fun deserializeConstant(value: ProtoBuf.Expression.ConstantValue): ConstantReference? = when (value) {
ProtoBuf.Expression.ConstantValue.TRUE -> BooleanConstantReference.TRUE
ProtoBuf.Expression.ConstantValue.FALSE -> BooleanConstantReference.FALSE
ProtoBuf.Expression.ConstantValue.NULL -> ConstantReference.NULL
}
private fun getComplexType(proto: ProtoBuf.Expression): ComplexExpressionType? {
val isOrSequence = proto.orArgumentCount != 0
val isAndSequence = proto.andArgumentCount != 0
return when {
isOrSequence && isAndSequence -> null
isOrSequence -> ComplexExpressionType.OR_SEQUENCE
isAndSequence -> ComplexExpressionType.AND_SEQUENCE
else -> null
}
}
private fun getPrimitiveType(proto: ProtoBuf.Expression): PrimitiveExpressionType? {
// Expected to be one element, but can be empty (unknown expression) or contain several elements (invalid data)
val expressionTypes: MutableList<PrimitiveExpressionType> = mutableListOf()
// Check for predicates
when {
proto.hasValueParameterReference() && proto.hasType() ->
expressionTypes.add(PrimitiveExpressionType.INSTANCE_CHECK)
proto.hasValueParameterReference() && proto.hasFlag(Flags.IS_NULL_CHECK_PREDICATE) ->
expressionTypes.add(PrimitiveExpressionType.NULLABILITY_CHECK)
}
// If message contains correct predicate, then predicate's type overrides type of value,
// even is message has one
if (expressionTypes.isNotEmpty()) {
return expressionTypes.singleOrNull()
}
// Otherwise, check if it is a value
when {
proto.hasValueParameterReference() && proto.valueParameterReference > 0 ->
expressionTypes.add(PrimitiveExpressionType.VALUE_PARAMETER_REFERENCE)
proto.hasValueParameterReference() && proto.valueParameterReference == 0 ->
expressionTypes.add(PrimitiveExpressionType.RECEIVER_REFERENCE)
proto.hasConstantValue() -> expressionTypes.add(PrimitiveExpressionType.CONSTANT)
}
return expressionTypes.singleOrNull()
}
private fun ProtoBuf.Expression.hasType(): Boolean = this.hasIsInstanceType() || this.hasIsInstanceTypeId()
private fun ProtoBuf.Expression.hasFlag(flag: Flags.BooleanFlagField) =
this.hasFlags() && flag.get(this.flags)
// Arguments of expressions with such types are never other expressions
private enum class PrimitiveExpressionType {
VALUE_PARAMETER_REFERENCE,
RECEIVER_REFERENCE,
CONSTANT,
INSTANCE_CHECK,
NULLABILITY_CHECK
}
// Expressions with such type can take other expressions as arguments.
// Additionally, for performance reasons, "complex expression" and "primitive expression"
// can co-exist in the one and the same message. If "primitive expression" is present
// in the current message, it is treated as the first argument of "complex expression".
private enum class ComplexExpressionType {
AND_SEQUENCE,
OR_SEQUENCE
}
}
}
@@ -27,4 +27,8 @@ class CompilerDeserializationConfiguration(languageVersionSettings: LanguageVers
override val skipMetadataVersionCheck = languageVersionSettings.getFlag(AnalysisFlag.skipMetadataVersionCheck)
override val isJvmPackageNameSupported = languageVersionSettings.supportsFeature(LanguageFeature.JvmPackageName)
override val returnsEffectAllowed: Boolean = languageVersionSettings.supportsFeature(LanguageFeature.ReturnsEffect)
override val callsInPlaceEffectAllowed: Boolean = languageVersionSettings.supportsFeature(LanguageFeature.CallsInPlaceEffect)
}
@@ -17,12 +17,13 @@
package org.jetbrains.kotlin.contracts.description
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.serialization.deserialization.ContractProvider
/**
* Essentially, this is a composition of two fields: value of type 'ContractDescription' and
* 'computation', which guarantees to initialize this field.
*/
class LazyContractProvider(private val computation: () -> Any?) {
class LazyContractProvider(private val computation: () -> Any?) : ContractProvider {
@Volatile
private var isComputed: Boolean = false
@@ -0,0 +1,201 @@
/*
* Copyright 2010-2017 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.kotlin.serialization
import org.jetbrains.kotlin.contracts.description.*
import org.jetbrains.kotlin.contracts.description.expressions.*
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ReceiverParameterDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
class ContractSerializer {
fun serializeContractOfFunctionIfAny(
functionDescriptor: FunctionDescriptor,
proto: ProtoBuf.Function.Builder,
parentSerializer: DescriptorSerializer
) {
val contractDescription = functionDescriptor.getUserData(ContractProviderKey)?.getContractDescription()
if (contractDescription != null) {
val worker = ContractSerializerWorker(parentSerializer)
proto.setContract(worker.contractProto(contractDescription))
}
}
private class ContractSerializerWorker(private val parentSerializer: DescriptorSerializer) {
fun contractProto(contractDescription: ContractDescription): ProtoBuf.Contract.Builder {
return ProtoBuf.Contract.newBuilder().apply {
contractDescription.effects.forEach { addEffect(effectProto(it, contractDescription)) }
}
}
private fun effectProto(effectDeclaration: EffectDeclaration, contractDescription: ContractDescription): ProtoBuf.Effect.Builder {
return ProtoBuf.Effect.newBuilder().apply {
fillEffectProto(this, effectDeclaration, contractDescription)
}
}
private fun fillEffectProto(builder: ProtoBuf.Effect.Builder, effectDeclaration: EffectDeclaration, contractDescription: ContractDescription) {
when (effectDeclaration) {
is ConditionalEffectDeclaration -> {
builder.setConclusionOfConditionalEffect(contractExpressionProto(effectDeclaration.condition, contractDescription))
fillEffectProto(builder, effectDeclaration.effect, contractDescription)
}
is ReturnsEffectDeclaration -> {
when {
effectDeclaration.value == ConstantReference.NOT_NULL -> builder.effectType = ProtoBuf.Effect.EffectType.RETURNS_NOT_NULL
effectDeclaration.value == ConstantReference.WILDCARD -> builder.effectType = ProtoBuf.Effect.EffectType.RETURNS_CONSTANT
else -> {
builder.effectType = ProtoBuf.Effect.EffectType.RETURNS_CONSTANT
builder.addEffectConstructorArgument(contractExpressionProto(effectDeclaration.value, contractDescription))
}
}
}
is CallsEffectDeclaration -> {
builder.effectType = ProtoBuf.Effect.EffectType.CALLS
builder.addEffectConstructorArgument(contractExpressionProto(effectDeclaration.variableReference, contractDescription))
val invocationKindProtobufEnum = invocationKindProtobufEnum(effectDeclaration.kind)
if (invocationKindProtobufEnum != null) {
builder.kind = invocationKindProtobufEnum
}
}
// TODO: Add else and do something like reporting issue?
}
}
private fun contractExpressionProto(contractDescriptionElement: ContractDescriptionElement, contractDescription: ContractDescription): ProtoBuf.Expression.Builder {
return contractDescriptionElement.accept(object : ContractDescriptionVisitor<ProtoBuf.Expression.Builder, Unit> {
override fun visitLogicalOr(logicalOr: LogicalOr, data: Unit): ProtoBuf.Expression.Builder {
val leftBuilder = logicalOr.left.accept(this, data)
return if (leftBuilder.andArgumentCount != 0) {
// can't flatten and re-use left builder
ProtoBuf.Expression.newBuilder().apply {
addOrArgument(leftBuilder)
addOrArgument(contractExpressionProto(logicalOr.right, contractDescription))
}
}
else {
// we can save some space by re-using left builder instead of nesting new one
leftBuilder.apply { addOrArgument(contractExpressionProto(logicalOr.right, contractDescription)) }
}
}
override fun visitLogicalAnd(logicalAnd: LogicalAnd, data: Unit): ProtoBuf.Expression.Builder {
val leftBuilder = logicalAnd.left.accept(this, data)
return if (leftBuilder.orArgumentCount != 0) {
// leftBuilder is already a sequence of Or-operators, so we can't re-use it
ProtoBuf.Expression.newBuilder().apply {
addAndArgument(leftBuilder)
addAndArgument(contractExpressionProto(logicalAnd.right, contractDescription))
}
}
else {
// we can save some space by re-using left builder instead of nesting new one
leftBuilder.apply { addAndArgument(contractExpressionProto(logicalAnd.right, contractDescription)) }
}
}
override fun visitLogicalNot(logicalNot: LogicalNot, data: Unit): ProtoBuf.Expression.Builder =
logicalNot.arg.accept(this, data).apply {
writeFlags(Flags.IS_NEGATED.invert(flags))
}
override fun visitIsInstancePredicate(isInstancePredicate: IsInstancePredicate, data: Unit): ProtoBuf.Expression.Builder {
// write variable
val builder = visitVariableReference(isInstancePredicate.arg, data)
// write rhs type
builder.isInstanceTypeId = parentSerializer.typeId(isInstancePredicate.type)
// set flags
builder.writeFlags(Flags.getContractExpressionFlags(isInstancePredicate.isNegated, false))
return builder
}
override fun visitIsNullPredicate(isNullPredicate: IsNullPredicate, data: Unit): ProtoBuf.Expression.Builder {
// get builder with variable embeded into it
val builder = visitVariableReference(isNullPredicate.arg, data)
// set flags
builder.writeFlags(Flags.getContractExpressionFlags(isNullPredicate.isNegated, true))
return builder
}
override fun visitConstantDescriptor(constantReference: ConstantReference, data: Unit): ProtoBuf.Expression.Builder {
val builder = ProtoBuf.Expression.newBuilder()
// write constant value
val constantValueProtobufEnum = constantValueProtobufEnum(constantReference)
if (constantValueProtobufEnum != null) {
builder.constantValue = constantValueProtobufEnum
}
return builder
}
override fun visitVariableReference(variableReference: VariableReference, data: Unit): ProtoBuf.Expression.Builder {
val builder = ProtoBuf.Expression.newBuilder()
val descriptor = variableReference.descriptor
val indexOfParameter = when (descriptor) {
is ReceiverParameterDescriptor -> 0
is ValueParameterDescriptor ->
contractDescription.ownerFunction.valueParameters.indexOf(descriptor).takeIf { it != -1 }?.inc()
else -> null
}
builder.valueParameterReference = indexOfParameter ?: return builder
return builder
}
}, Unit)
}
private fun ProtoBuf.Expression.Builder.writeFlags(newFlagsValue: Int) {
if (flags != newFlagsValue) {
flags = newFlagsValue
}
}
private fun invocationKindProtobufEnum(kind: InvocationKind): ProtoBuf.Effect.InvocationKind? = when (kind) {
InvocationKind.AT_MOST_ONCE -> ProtoBuf.Effect.InvocationKind.AT_MOST_ONCE
InvocationKind.EXACTLY_ONCE -> ProtoBuf.Effect.InvocationKind.EXACTLY_ONCE
InvocationKind.AT_LEAST_ONCE -> ProtoBuf.Effect.InvocationKind.AT_LEAST_ONCE
InvocationKind.UNKNOWN -> null
}
private fun constantValueProtobufEnum(constantReference: ConstantReference): ProtoBuf.Expression.ConstantValue? = when (constantReference) {
BooleanConstantReference.TRUE -> ProtoBuf.Expression.ConstantValue.TRUE
BooleanConstantReference.FALSE -> ProtoBuf.Expression.ConstantValue.FALSE
ConstantReference.NULL -> ProtoBuf.Expression.ConstantValue.NULL
ConstantReference.NOT_NULL -> throw IllegalStateException(
"Internal error during serialization of function contract: NOT_NULL constant isn't denotable in protobuf format. " +
"Its serialization should be handled at higher level"
)
ConstantReference.WILDCARD -> null
else -> throw IllegalArgumentException("Unknown constant: $constantReference")
}
}
}
@@ -49,6 +49,8 @@ class DescriptorSerializer private constructor(
private val versionRequirementTable: MutableVersionRequirementTable,
private val serializeTypeTableToFunction: Boolean
) {
private val contractSerializer = ContractSerializer()
fun serialize(message: MessageLite): ByteArray {
return ByteArrayOutputStream().apply {
stringTable.serializeTo(this)
@@ -150,7 +152,6 @@ class DescriptorSerializer private constructor(
}
extension.serializeClass(classDescriptor, builder)
return builder
}
@@ -295,6 +296,8 @@ class DescriptorSerializer private constructor(
builder.versionRequirement = writeVersionRequirement(LanguageFeature.Coroutines)
}
contractSerializer.serializeContractOfFunctionIfAny(descriptor, builder, this)
extension.serializeFunction(descriptor, builder)
return builder
@@ -452,9 +455,9 @@ class DescriptorSerializer private constructor(
return builder
}
private fun typeId(type: KotlinType): Int = typeTable[type(type)]
internal fun typeId(type: KotlinType): Int = typeTable[type(type)]
private fun type(type: KotlinType): ProtoBuf.Type.Builder {
internal fun type(type: KotlinType): ProtoBuf.Type.Builder {
val builder = ProtoBuf.Type.newBuilder()
if (type.isError) {
@@ -661,6 +664,8 @@ class DescriptorSerializer private constructor(
private fun getTypeParameterId(descriptor: TypeParameterDescriptor): Int =
typeParameters.intern(descriptor)
companion object {
@JvmStatic
fun createTopLevel(extension: SerializerExtension): DescriptorSerializer {