All Downloads are FREE. Search and download functionalities are using the official Maven repository.

arrow.meta.plugins.proofs.phases.resolve.ProofResolutionRules.kt Maven / Gradle / Ivy

There is a newer version: 2.0.0-alpha.6
Show newest version
package arrow.meta.plugins.proofs.phases.resolve

import arrow.meta.Meta
import arrow.meta.diagnostic.MetaErrors
import arrow.meta.diagnostic.MetaErrors.AmbiguousProof
import arrow.meta.diagnostic.MetaErrors.AmbiguousProofForSupertype
import arrow.meta.diagnostic.MetaErrors.CycleOnGivenProof
import arrow.meta.diagnostic.MetaErrors.OwnershipViolatedProof
import arrow.meta.diagnostic.MetaErrors.UnresolvedGivenCallSite
import arrow.meta.diagnostic.MetaErrors.UnresolvedGivenProof
import arrow.meta.internal.Noop
import arrow.meta.phases.CompilerContext
import arrow.meta.phases.Composite
import arrow.meta.phases.ExtensionPhase
import arrow.meta.phases.analysis.exists
import arrow.meta.phases.analysis.traverseFilter
import arrow.meta.plugins.proofs.phases.ArrowCompileTime
import arrow.meta.plugins.proofs.phases.CallableMemberProof
import arrow.meta.plugins.proofs.phases.ClassProof
import arrow.meta.plugins.proofs.phases.GivenProof
import arrow.meta.plugins.proofs.phases.GivenProofResolution
import arrow.meta.plugins.proofs.phases.ObjectProof
import arrow.meta.plugins.proofs.phases.Proof
import arrow.meta.plugins.proofs.phases.allGivenProofs
import arrow.meta.plugins.proofs.phases.contextualAnnotations
import arrow.meta.plugins.proofs.phases.givenProof
import arrow.meta.plugins.proofs.phases.hasAnnotation
import arrow.meta.plugins.proofs.phases.isGivenContextProof
import arrow.meta.plugins.proofs.phases.isProof
import arrow.meta.plugins.proofs.phases.proof
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.SourceElement
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingTrace
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
import org.jetbrains.kotlin.resolve.calls.model.DefaultValueArgument
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedContainerSource
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.isTypeParameter
import org.jetbrains.kotlin.types.typeUtil.isUnit
import org.jetbrains.kotlin.utils.addToStdlib.safeAs

internal fun Meta.proofResolutionRules(): ExtensionPhase =
  Composite(
    analysis(
      doAnalysis = Noop.nullable7(),
      analysisCompleted = { _, _, bindingTrace, files ->
        resolutionRules(bindingTrace, files)
        null
      }
    ),
    callChecker { resolvedCall, reportOn, context ->
      callSiteResolution(resolvedCall, reportOn, context)
    }
  )

internal fun CompilerContext.resolutionRules(trace: BindingTrace, files: Collection): Unit {
  // General rules
  files.forEach { file: KtFile ->
    reportProhibitedPublishedInternalOrphans(trace, file)
    reportOwnershipViolations(trace, file)
  }
  // Rule-set for GivenProofs
  allGivenProofs().run {
    reportUnresolvedGivenProofs(trace, messageCollector)
    reportDisallowedUserDefinedAmbiguities(trace)
    reportSkippedProofsDueToAmbiguities { proof, ambiguities ->
      messageCollector?.report(CompilerMessageSeverity.ERROR, "Please Provide an internal Proof")
        ?: println("TODO for skipped Proofs:$proof with ambiguities:$ambiguities")
    }
  }
}

internal fun CompilerContext.callSiteResolution(
  resolvedCall: ResolvedCall<*>,
  reportOn: PsiElement,
  context: CallCheckerContext
): Unit =
  reportOn.parent.safeAs()?.let {
    reportUnresolvedGivenCallSite(resolvedCall, it, context.trace)
  }
    ?: Unit

val ClassDescriptor.inlinedType: KotlinType?
  get() =
    takeIf { it.isInline }?.run { unsubstitutedPrimaryConstructor?.valueParameters?.first()?.type }

fun CompilerContext.unresolvedGivenCallSite(
  call: ResolvedCall<*>
): List> =
  call.resultingDescriptor.valueParameters
    .filter { v ->
      v.containingDeclaration.annotations.hasAnnotation(ArrowCompileTime) &&
        call.valueArguments[v] == DefaultValueArgument.DEFAULT &&
        !v.type.isUnit()
    }
    .mapNotNull { v ->
      val contextFqName = v.contextualAnnotations().firstOrNull()
      if (contextFqName != null) {
        val givenProofResolution = givenProof(contextFqName, v.type)
        if (givenProofResolution.givenProof == null) null to v else givenProofResolution to v
      } else null
    }

fun prohibitedPublishedInternalOrphans(
  bindingContext: BindingContext,
  file: KtFile
): List =
  file.traverseFilter(KtDeclaration::class.java) { declaration ->
    declaration.isPublishedInternalOrphan(bindingContext)
  }

fun KtDeclaration.isPublishedInternalOrphan(bindingContext: BindingContext): KtDeclaration? =
    takeIf {
  it.isProof(bindingContext) &&
    it.hasAnnotation(bindingContext, StandardNames.FqNames.publishedApi) &&
    it.hasModifier(KtTokens.INTERNAL_KEYWORD)
}

fun CompilerContext.ownershipViolations(
  trace: BindingContext,
  file: KtFile
): List> =
  file.traverseFilter(KtDeclaration::class.java) { declaration ->
    declaration.isViolatingOwnershipRule(trace, this)
  }

fun KtDeclaration.isViolatingOwnershipRule(
  bindingContext: BindingContext,
  ctx: CompilerContext
): Pair? =
  takeIf { it.isProof(bindingContext) }?.let {
    ctx
      .proof()
      .firstOrNull {
        it.through == bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, this)
      }
      ?.takeIf {
        !hasModifier(KtTokens.INTERNAL_KEYWORD) &&
          (when (it) {
            is GivenProof -> !it.to.isUserOwned()
          })
      }
      ?.let { this to it }
  }

/**
 * A type is user-owned, when at least one position of the type signature is a user type in the
 * sources. e.g.: `org.core.Semigroup` materialises into `A -> F -> org.core.Semigroup`
 * Thereby the user needs to own either `F`, `A` or `org.core.Semigroup` to publish a proof. `F` or
 * `A` can't be type parameters to be user-owned.
 */
fun KotlinType.isUserOwned(): Boolean =
  (hasUserSource() && !isTypeParameter()) || arguments.any { it.type.isUserOwned() }

fun KotlinType.hasUserSource(): Boolean =
  constructor.declarationDescriptor?.run {
    source !is DeserializedContainerSource && source != SourceElement.NO_SOURCE
  }
    ?: false

fun  Map>.disallowedAmbiguities(): List>> =
  mapNotNull { (_, proofs) ->
      proofs
        .exists { p1, p2 ->
          val a = p1.through.safeAs()?.visibility
          val b = p2.through.safeAs()?.visibility
          (a == DescriptorVisibilities.PUBLIC && b == DescriptorVisibilities.PUBLIC ||
            (a == DescriptorVisibilities.INTERNAL && b == DescriptorVisibilities.INTERNAL)) &&
            p1.isContextAmbiguous(p2)
          // TODO: Loosen the rule to allow package scoped proofs when they have the same
          // package-info
        }
        .filter { (_, v) -> v.isNotEmpty() } // filter out proofs with conflicts
    }
    .flatten()

fun  Map>.disallowedUserDefinedAmbiguities():
  List, List>> =
  disallowedAmbiguities().mapNotNull { (proof, others) ->
    // collect those with a SourceElement, which the User is responsible of
    proof.through.findPsi().safeAs()?.let { (proof to it) to others }
  }

/**
 * additionally to the Docs in [reportDisallowedUserDefinedAmbiguities]. The following extensions
 * sends warnings, which proofs are being skipped and a prompt to the user to define an internal
 * orphan to resolve coherence.
 */
fun  Map>.skippedProofsDueToAmbiguities(): List>> =
  disallowedAmbiguities().filter { (proof, others) ->
    // collect those without a SourceElement, which the user is needs to override
    with(proof.through as DeclarationDescriptorWithVisibility) {
      findPsi() == null &&
        (visibility ==
          DescriptorVisibilities
            .INTERNAL || // case: instrumented dependencies that publish internal proofs
        visibility == DescriptorVisibilities.PUBLIC) &&
        others.any {
          // case: local project publishes public proof over non-owned types
          with(it.through) { visibility == DescriptorVisibilities.PUBLIC && findPsi() == null }
        }
    }
  }

/**
 * `unresolved` that is a GivenProof that has Parameters e.g.: [ClassProof], [CallableMemberProof]
 * and those don't have default values or are not being semi-inductively resolved by other
 * GivenProof's.
 */
fun Map>.unresolvedGivenProofs():
  Map> = mapValues { (_, proofs) ->
  proofs
    .map {
      val resolutionResult = it.isResolved(this, mutableSetOf())
      resolutionResult to it
    }
    .filter { (result, _) -> !result.first }
    .toMap()
}

/** if its resolved and any cycled proofs found that otherwise would have led to non-termination */
typealias ResolutionResult = Pair>

fun GivenProof.isResolved(
  others: Map>,
  previousProofs: MutableSet
): ResolutionResult =
  when (this) {
    is ObjectProof ->
      true to
        emptySet() // object proofs are resolved automatically, as they do not have constructors
    is ClassProof -> isResolved(others, previousProofs)
    is CallableMemberProof -> isResolved(others, previousProofs)
  }

/**
 * in IR the primaryConstructor is chosen see
 * [arrow.meta.plugins.proofs.phases.resolve.asGivenProof] TODO: Check if the defaultValue is
 * resolved
 */
fun ClassProof.isResolved(
  others: Map>,
  previousProofs: MutableSet
): ResolutionResult =
  if (this in previousProofs) false to previousProofs
  else
    through.unsubstitutedPrimaryConstructor?.valueParameters
      ?.all { param ->
        if (param.annotations.any { it.isGivenContextProof() })
          others.getOrDefault(param.type, emptyList()).any {
            previousProofs.add(this)
            it.isResolved(others, previousProofs).first
          }
        else param.declaresDefaultValue()
      }
      ?.let { it to previousProofs }
      ?: false to previousProofs

/** TODO: Check if the defaultValue is resolved */
fun CallableMemberProof.isResolved(
  others: Map>,
  previousProofs: MutableSet
): ResolutionResult =
  if (this in previousProofs) false to previousProofs
  else
    through.valueParameters.all { param ->
      if (!param.type.isTypeParameter() && param.annotations.any { it.isGivenContextProof() })
        others.getOrDefault(param.type, emptyList()).any {
          previousProofs.add(this)
          it.isResolved(others, previousProofs).first
        }
      else true
    } to previousProofs

fun Map>.reportUnresolvedGivenProofs(
  trace: BindingTrace,
  msg: MessageCollector?
): Unit =
  unresolvedGivenProofs().forEach { (type, results) ->
    results.forEach { (t, proof) ->
      val (_, cycles) = t
      proof.through.findPsi()?.safeAs()?.let { element ->
        if (cycles.isNotEmpty()) {
          trace.report(CycleOnGivenProof.on(element, type, cycles))
        } else {
          trace.report(UnresolvedGivenProof.on(element, type))
        }
      }
        ?: msg?.report(
          CompilerMessageSeverity.WARNING,
          "The GivenProof ${proof.through.fqNameSafe.asString()} from the project dependencies on the type ${
          DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(
            type
          )
        } can't be semi-inductively resolved and won't be considered in resolution."
        )
    }
  }

/**
 * the strategy that follows is that authors are responsible for a coherent resolution of the
 * project. Either by disambiguating proofs through a proper scope or disambiguating dependency
 * proofs that lead to undefined behavior of proof resolution for the project or 3rd parties
 * depending on coherence.
 */
private fun  Map>.reportDisallowedUserDefinedAmbiguities(
  trace: BindingTrace
): Unit =
  disallowedUserDefinedAmbiguities().toMap().forEach { (proof, f), conflicts ->
    trace.report(AmbiguousProof.on(f, proof, conflicts))
  }

/**
 * Internal proofs are not permitted to be published, due to coherence reasons leading to
 * ambiguities.
 */
private fun reportProhibitedPublishedInternalOrphans(trace: BindingTrace, file: KtFile): Unit =
  prohibitedPublishedInternalOrphans(trace.bindingContext, file).forEach {
    trace.report(MetaErrors.PublishedInternalOrphan.on(it))
  }

/**
 * Public Proofs are only valid, if they don't impose inconsistencies in the resolution process.
 * That is the associated type or types in the proof have to be user owned.
 * @see isUserOwned
 */
private fun CompilerContext.reportOwnershipViolations(trace: BindingTrace, file: KtFile): Unit =
  ownershipViolations(trace.bindingContext, file).forEach { (declaration, proof) ->
    trace.report(OwnershipViolatedProof.on(declaration, proof))
  }

private fun  Map>.reportSkippedProofsDueToAmbiguities(
  f: (proof: A, ambiguities: List) -> Unit
): Unit = skippedProofsDueToAmbiguities().toMap().forEach(f)

private fun CompilerContext.reportUnresolvedGivenCallSite(
  call: ResolvedCall<*>,
  element: KtExpression,
  trace: BindingTrace
): Unit =
  unresolvedGivenCallSite(call).let { values ->
    values.forEach { (resolution, v) ->
      if (resolution?.ambiguousProofs?.isNotEmpty() == true &&
          resolution.ambiguousProofs.size > 1 &&
          resolution.givenProof != null
      ) {
        trace.report(
          AmbiguousProofForSupertype.on(
            element,
            resolution.targetType,
            resolution.givenProof,
            resolution.ambiguousProofs
          )
        )
      }
      if (resolution?.givenProof == null) {
        reportMissingInductiveDependencies(v, trace, element, call)
        trace.report(UnresolvedGivenCallSite.on(element, call, v.type))
      }
    }
  }

private fun CompilerContext.reportMissingInductiveDependencies(
  it: ValueParameterDescriptor,
  trace: BindingTrace,
  element: KtExpression,
  call: ResolvedCall<*>
) {
  if (it.type.constructor.declarationDescriptor?.annotations?.any { it.isGivenContextProof() } ==
      true
  ) {
    val dcl = it.type.constructor.declarationDescriptor
    if (dcl is ClassDescriptor) {
      dcl.constructors.firstOrNull { it.isPrimary }?.valueParameters?.forEach { valueParam ->
        val contextFqName = valueParam.contextualAnnotations().firstOrNull()
        val parameterProof = contextFqName?.let { givenProof(it, valueParam.type) }
        if (parameterProof?.givenProof == null)
          trace.report(UnresolvedGivenCallSite.on(element, call, valueParam.type))
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy