org.jetbrains.kotlin.backend.common.IrValidator.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-compiler-embeddable Show documentation
Show all versions of kotlin-compiler-embeddable Show documentation
the Kotlin compiler embeddable
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.backend.common
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.config.IrVerificationMode
import org.jetbrains.kotlin.ir.IrBuiltIns
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.util.DeclarationParentsVisitor
import org.jetbrains.kotlin.ir.util.dump
import org.jetbrains.kotlin.ir.util.render
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
typealias ReportIrValidationError = (IrFile?, IrElement, String, List) -> Unit
internal class IrValidatorConfig(
val checkTypes: Boolean = true,
val checkProperties: Boolean = false,
val checkValueScopes: Boolean = false,
val checkTypeParameterScopes: Boolean = false,
val checkCrossFileFieldUsage: Boolean = false,
val checkVisibilities: Boolean = false,
val checkInlineFunctionUseSites: InlineFunctionUseSiteChecker? = null,
)
fun interface InlineFunctionUseSiteChecker {
/**
* Check if the given use site of the inline function is permitted at the current phase of IR validation.
*
* Example 1: Check use sites after inlining all private functions.
* It is permitted to have only use sites of non-private functions in the whole IR tree. So, for a use site
* of a private inline function we should return `false` if it is met in the IR. For any other use site
* we should return `true` (== permitted).
*
* Example 2: Check use sites after inlining all functions.
* Normally, no use sites of inline functions should remain in the whole IR tree. So, if we met one we shall
* return `false` (== not permitted). However, there are a few exceptions that are temporarily permitted.
* For example, `inline external` intrinsics in Native (KT-66734).
*/
fun isPermitted(inlineFunctionUseSite: IrMemberAccessExpression): Boolean
}
private class IrValidator(
irBuiltIns: IrBuiltIns,
val config: IrValidatorConfig,
val reportError: ReportIrValidationError
) : IrElementVisitorVoid {
var currentFile: IrFile? = null
private val parentChain = mutableListOf()
override fun visitFile(declaration: IrFile) {
currentFile = declaration
super.visitFile(declaration)
if (config.checkValueScopes) {
IrValueScopeValidator(this::error, parentChain).check(declaration)
}
if (config.checkTypeParameterScopes) {
IrTypeParameterScopeValidator(this::error, parentChain).check(declaration)
}
if (config.checkCrossFileFieldUsage) {
declaration.acceptVoid(IrFieldCrossFileAccessValidator(declaration, reportError))
}
if (config.checkVisibilities) {
declaration.acceptVoid(IrVisibilityChecker(declaration.module, declaration, reportError))
}
config.checkInlineFunctionUseSites?.let {
declaration.acceptVoid(NoInlineFunctionUseSitesValidator(declaration, reportError, it))
}
}
private fun error(element: IrElement, message: String) {
reportError(currentFile, element, message, parentChain)
}
private val elementChecker = CheckIrElementVisitor(irBuiltIns, this::error, config)
override fun visitElement(element: IrElement) {
element.acceptVoid(elementChecker)
parentChain.push(element)
element.acceptChildrenVoid(this)
parentChain.pop()
}
}
private fun IrElement.checkDeclarationParents(reportError: ReportIrValidationError) {
val checker = CheckDeclarationParentsVisitor()
accept(checker, null)
if (checker.errors.isNotEmpty()) {
val expectedParents = LinkedHashSet()
reportError(
null,
this,
buildString {
append("Declarations with wrong parent: ")
append(checker.errors.size)
append("\n")
checker.errors.forEach {
append("declaration: ")
append(it.declaration.render())
append("\nexpectedParent: ")
append(it.expectedParent.render())
append("\nactualParent: ")
append(it.actualParent?.render())
}
append("\nExpected parents:\n")
expectedParents.forEach {
append(it.dump())
}
},
emptyList(),
)
}
}
private class CheckDeclarationParentsVisitor : DeclarationParentsVisitor() {
class Error(val declaration: IrDeclaration, val expectedParent: IrDeclarationParent, val actualParent: IrDeclarationParent?)
val errors = ArrayList()
override fun handleParent(declaration: IrDeclaration, actualParent: IrDeclarationParent) {
try {
val assignedParent = declaration.parent
if (assignedParent != actualParent) {
errors.add(Error(declaration, assignedParent, actualParent))
}
} catch (e: Exception) {
errors.add(Error(declaration, actualParent, null))
}
}
}
open class IrValidationError(message: String? = null, cause: Throwable? = null) : IllegalStateException(message, cause)
class DuplicateIrNodeError(element: IrElement) : IrValidationError(element.render())
/**
* Verifies common IR invariants that should hold in all the backends.
*/
private fun performBasicIrValidation(
element: IrElement,
irBuiltIns: IrBuiltIns,
validatorConfig: IrValidatorConfig,
reportError: ReportIrValidationError,
) {
val validator = IrValidator(irBuiltIns, validatorConfig, reportError)
try {
element.acceptVoid(validator)
} catch (e: DuplicateIrNodeError) {
// Performing other checks may cause e.g. infinite recursion.
return
}
element.checkDeclarationParents(reportError)
}
/**
* [IrValidationContext] is responsible for collecting validation errors, logging them and optionally throwing [IrValidationError]
* (if the verification mode passed to [validateIr] is [IrVerificationMode.ERROR])
*/
sealed interface IrValidationContext {
/**
* Logs the validation error into the underlying [MessageCollector].
*/
fun reportIrValidationError(
file: IrFile?,
element: IrElement,
message: String,
phaseName: String,
parentChain: List = emptyList(),
)
/**
* Allows to abort the compilation process if after or during validating the IR there were errors and the verification mode is
* [IrVerificationMode.ERROR].
*/
fun throwValidationErrorIfNeeded()
/**
* Verifies common IR invariants that should hold in all the backends.
*
* Reports errors to [CommonBackendContext.messageCollector].
*
* **Note:** this method does **not** throw [IrValidationError]. Use [throwValidationErrorIfNeeded] for checking for errors and throwing
* [IrValidationError]. This gives the caller the opportunity to perform additional (for example, backend-specific) validation before
* aborting. The caller decides when it's time to abort.
*/
fun performBasicIrValidation(
fragment: IrElement,
irBuiltIns: IrBuiltIns,
phaseName: String,
checkProperties: Boolean = false,
checkTypes: Boolean = false,
checkVisibilities: Boolean = false,
checkCrossFileFieldUsage: Boolean = false,
checkValueScopes: Boolean = false,
checkTypeParameterScopes: Boolean = false,
checkInlineFunctionUseSites: InlineFunctionUseSiteChecker? = null,
) {
performBasicIrValidation(
fragment,
irBuiltIns,
IrValidatorConfig(
checkTypes,
checkProperties,
checkValueScopes,
checkTypeParameterScopes,
checkCrossFileFieldUsage,
checkVisibilities,
checkInlineFunctionUseSites,
),
) { file, element, message, parentChain ->
reportIrValidationError(file, element, message, phaseName, parentChain)
}
}
}
private class IrValidationContextImpl(
private val messageCollector: MessageCollector,
private val mode: IrVerificationMode
) : IrValidationContext {
private var hasValidationErrors: Boolean = false
override fun reportIrValidationError(
file: IrFile?,
element: IrElement,
message: String,
phaseName: String,
parentChain: List,
) {
val severity = when (mode) {
IrVerificationMode.WARNING -> CompilerMessageSeverity.WARNING
IrVerificationMode.ERROR -> CompilerMessageSeverity.ERROR
IrVerificationMode.NONE -> return
}
hasValidationErrors = true
val phaseMessage = if (phaseName.isNotEmpty()) "$phaseName: " else ""
messageCollector.report(
severity,
buildString {
append("[IR VALIDATION] ")
append(phaseMessage)
appendLine(message)
append(element.render())
for ((i, parent) in parentChain.asReversed().withIndex()) {
appendLine()
append(" ".repeat(i + 1))
append("inside ")
append(parent.render())
}
},
file?.let(element::getCompilerMessageLocation),
)
}
override fun throwValidationErrorIfNeeded() {
if (hasValidationErrors && mode == IrVerificationMode.ERROR) {
throw IrValidationError()
}
}
}
/**
* Logs validation errors encountered during the execution of the [runValidationRoutines] closure into [messageCollector].
*
* If [mode] is [IrVerificationMode.ERROR], throws [IrValidationError] after [runValidationRoutines] has finished,
* thus allowing to collect as many errors as possible instead of aborting after the first one.
*/
fun validateIr(
messageCollector: MessageCollector,
mode: IrVerificationMode,
runValidationRoutines: IrValidationContext.() -> Unit,
) {
if (mode == IrVerificationMode.NONE) return
val validationContext = IrValidationContextImpl(messageCollector, mode)
validationContext.runValidationRoutines()
validationContext.throwValidationErrorIfNeeded()
}