fixup! Add PowerAssert annotations and support in compiler-plugin

* Add metadata to transformed functions and containing classes
* Support function overrides and super calls
* Support recursive calls
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt
index f83de34..5a05f12 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt
@@ -5,70 +5,77 @@
 
 package org.jetbrains.kotlin.powerassert
 
-import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
 import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter
-import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrClass
+import org.jetbrains.kotlin.ir.declarations.IrDeclaration
+import org.jetbrains.kotlin.ir.declarations.IrMetadataSourceOwner
 import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
-import org.jetbrains.kotlin.ir.declarations.IrValueParameter
-import org.jetbrains.kotlin.ir.expressions.IrCall
-import org.jetbrains.kotlin.ir.expressions.IrExpression
-import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
 import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
-import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
 import org.jetbrains.kotlin.ir.expressions.impl.fromSymbolOwner
 import org.jetbrains.kotlin.ir.irAttribute
 import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
-import org.jetbrains.kotlin.ir.types.classFqName
-import org.jetbrains.kotlin.ir.types.makeNullable
 import org.jetbrains.kotlin.ir.util.*
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
 import org.jetbrains.kotlin.name.JvmStandardClassIds
 import org.jetbrains.kotlin.name.Name
 
+var IrSimpleFunction.explainedDispatchSymbol: IrSimpleFunctionSymbol? by irAttribute(followAttributeOwner = false)
+
 class ExplainCallFunctionFactory(
-    private val module: IrModuleFragment,
     private val context: IrPluginContext,
     private val builtIns: PowerAssertBuiltIns,
 ) {
-    private var IrSimpleFunction.explainedDispatchFunctionSymbol: IrSimpleFunctionSymbol? by irAttribute(followAttributeOwner = false)
+    private val memorizedClassMetadata = mutableSetOf<IrClass>()
 
     fun find(function: IrSimpleFunction): IrSimpleFunctionSymbol? {
-        function.explainedDispatchFunctionSymbol?.let { return it }
+        // If there is an explained symbol:
+        // 1. Function was transformed to generate an explained overload.
+        // 2. Explained overload was already found and saved for faster lookup.
+        function.explainedDispatchSymbol?.let { return it }
 
-        // TODO this never works because... the generated function has no metadata? need to create an FIR function as well?
-        val callableId = function.callableId.copy(Name.identifier(function.name.identifier + "\$explained"))
-        context.referenceFunctions(callableId).singleOrNull { it.isSyntheticFor(function) }?.let { return it }
-
-        // TODO is this the best way to handle compilation unit checks?
-//        if (function.fileOrNull !in module.files) return null
+        // Metadata indicates the function was transformed but is not in the current compilation unit.
+        // Generate a stub-function so a symbol exists which can be called.
+        val parentClass = function.parent as? IrClass
+        getPowerAssertMetadata(parentClass ?: function) ?: return null
         return generate(function).symbol
     }
 
-    fun generate(function: IrSimpleFunction): IrSimpleFunction {
-        val newFunction = function.deepCopyWithSymbols(function.parent).apply {
+    fun generate(originalFunction: IrSimpleFunction): IrSimpleFunction {
+        originalFunction.explainedDispatchSymbol?.let { return it.owner }
+
+        val explainedFunction = originalFunction.deepCopyWithSymbols(originalFunction.parent)
+        explainedFunction.apply {
             origin = FUNCTION_FOR_EXPLAIN_CALL
-            name = Name.identifier(function.name.identifier + "\$explained")
-            annotations = annotations.filter { !it.isAnnotationWithEqualFqName(builtIns.explainCallType.classFqName!!) } +
-                    createJvmSyntheticAnnotation()
-            addValueParameter {
+            name = Name.identifier("${originalFunction.name.identifier}\$explained")
+            annotations = annotations.filter { !it.isAnnotation(PowerAssertBuiltIns.explainCallFqName) }
+            annotations += createJvmSyntheticAnnotation() // TODO is this needed?
+            val explanationParameter = addValueParameter {
                 name = Name.identifier("\$explanation") // TODO what if there's another property with this name?
                 type = builtIns.callExplanationType
             }
+
+            overriddenSymbols = originalFunction.overriddenSymbols.map { generate(it.owner).symbol }
+
+            // Transform the generated function to use the `$explanation` parameter instead of CallExplain.explanation.
+            transformChildrenVoid(ExplainCallGetExplanationTransformer(builtIns, explanationParameter))
+            // Transform the generated function to propagate the `$explanation` parameter during recursive or super-calls.
+            transformChildrenVoid(ExplainedSelfCallTransformer(originalFunction, explanationParameter))
+
         }
 
-        // Transform the generated function to use the `$explanation` parameter instead of Explain.explanation.
-        val diagramParameter = newFunction.valueParameters.last()
-        newFunction.transformChildrenVoid(DiagramDispatchTransformer(diagramParameter, context))
-        function.explainedDispatchFunctionSymbol = newFunction.symbol
+        // Save the explained function to the original function to make overload lookup easier.
+        originalFunction.explainedDispatchSymbol = explainedFunction.symbol
 
-        // Transform the original function to use `null` instead of Explain.explanation.
-        // This keeps the code from throwing an error when Explain.explanation.
-        // This in turn helps make sure the compiler-plugin is applied to functions which use `@Explain`.
-        // TODO should this be in the transformer and not here?
-        function.transformChildrenVoid(DiagramDispatchTransformer(explanation = null, context))
+        // Write custom metadata to indicate the original function was indeed compiled with the plugin.
+        // This allows callers to be confident the explained function exists, even when calling from a different compilation unit.
+        val parentClass = originalFunction.parent as? IrClass
+        when {
+            parentClass == null -> addPowerAssertMetadata(originalFunction)
+            memorizedClassMetadata.add(parentClass) -> addPowerAssertMetadata(parentClass)
+        }
 
-        return newFunction
+        return explainedFunction
     }
 
     private fun createJvmSyntheticAnnotation(): IrConstructorCallImpl {
@@ -81,36 +88,17 @@
         )
     }
 
-    private class DiagramDispatchTransformer(
-        private val explanation: IrValueParameter?,
-        private val context: IrPluginContext,
-    ) : IrElementTransformerVoidWithContext() {
-        override fun visitExpression(expression: IrExpression): IrExpression {
-            return when {
-                isExplanation(expression) -> when (explanation) {
-                    null -> IrConstImpl.constNull(expression.startOffset, expression.endOffset, context.irBuiltIns.anyType.makeNullable())
-                    else -> IrGetValueImpl(expression.startOffset, expression.endOffset, explanation.type, explanation.symbol)
-                }
-                else -> super.visitExpression(expression)
-            }
+    private fun <E> addPowerAssertMetadata(declaration: E)
+            where E : IrDeclaration, E : IrMetadataSourceOwner {
+        if (declaration.metadata != null) {
+            context.metadataDeclarationRegistrar
+                .addCustomMetadataExtension(declaration, PowerAssertBuiltIns.PLUGIN_ID, builtIns.metadata.data)
         }
-
-        private fun isExplanation(expression: IrExpression): Boolean =
-            (expression as? IrCall)?.symbol?.owner?.kotlinFqName == PowerAssertGetDiagram
     }
 
-    private fun IrSimpleFunctionSymbol.isSyntheticFor(function: IrSimpleFunction): Boolean {
-        // TODO need to consider type parameters and how they differ.
-
-        val owner = owner
-        if (function.dispatchReceiverParameter?.type != owner.dispatchReceiverParameter?.type) return false
-        if (function.extensionReceiverParameter?.type != owner.extensionReceiverParameter?.type) return false
-
-        if (function.valueParameters.size != owner.valueParameters.size - 1) return false
-        for (index in function.valueParameters.indices) {
-            if (function.valueParameters[index].type != owner.valueParameters[index].type) return false
-        }
-
-        return owner.valueParameters.last().type == builtIns.callExplanationType
+    private fun <E> getPowerAssertMetadata(declaration: E): ByteArray?
+            where E : IrDeclaration, E : IrMetadataSourceOwner {
+        return context.metadataDeclarationRegistrar.getCustomMetadataExtension(declaration, PowerAssertBuiltIns.PLUGIN_ID)
     }
 }
+
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionTransformer.kt
new file mode 100644
index 0000000..610fe45
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionTransformer.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.powerassert
+
+import org.jetbrains.kotlin.backend.common.DeclarationTransformer
+import org.jetbrains.kotlin.ir.declarations.IrDeclaration
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
+
+class ExplainCallFunctionTransformer(
+    private val builtIns: PowerAssertBuiltIns,
+    private val factory: ExplainCallFunctionFactory,
+) : DeclarationTransformer {
+    private val transformer = ExplainCallGetExplanationTransformer(builtIns, parameter = null)
+
+    override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
+        if (declaration is IrSimpleFunction) {
+            if (declaration.hasAnnotationOrOverridden(builtIns.explainCallClass)) {
+                return lower(declaration)
+            }
+        }
+
+        return null
+    }
+
+    private fun lower(originalFunction: IrSimpleFunction): List<IrSimpleFunction> {
+        val explainedFunction = factory.generate(originalFunction)
+
+        // Transform the original function to use `null` instead of ExplainCall.explanation.
+        // This keeps the code from throwing an error when ExplainCall.explanation is used.
+        // This in turn helps make sure the compiler-plugin is applied to functions which use `@ExplainCall`.
+        originalFunction.transformChildrenVoid(transformer)
+
+        return listOf(originalFunction, explainedFunction)
+    }
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallGetExplanationTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallGetExplanationTransformer.kt
new file mode 100644
index 0000000..8d31069
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallGetExplanationTransformer.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2025 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.powerassert
+
+import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
+import org.jetbrains.kotlin.ir.declarations.IrValueParameter
+import org.jetbrains.kotlin.ir.expressions.IrCall
+import org.jetbrains.kotlin.ir.expressions.IrExpression
+import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
+import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
+import org.jetbrains.kotlin.ir.types.makeNullable
+import org.jetbrains.kotlin.ir.util.kotlinFqName
+
+/**
+ * Replaces all calls to `ExplainCall.explanation` with either `null` or a parameter access.
+ */
+class ExplainCallGetExplanationTransformer(
+    private val builtIns: PowerAssertBuiltIns,
+    private val parameter: IrValueParameter?,
+) : IrElementTransformerVoidWithContext() {
+    override fun visitExpression(expression: IrExpression): IrExpression {
+        return when {
+            isGetExplanation(expression) -> when (parameter) {
+                null -> IrConstImpl.constNull(expression.startOffset, expression.endOffset, builtIns.callExplanationType.makeNullable())
+                else -> IrGetValueImpl(expression.startOffset, expression.endOffset, parameter.type, parameter.symbol)
+            }
+            else -> super.visitExpression(expression)
+        }
+    }
+
+    private fun isGetExplanation(expression: IrExpression): Boolean =
+        (expression as? IrCall)?.symbol?.owner?.kotlinFqName == ExplainCallGetExplanation
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainedSelfCallTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainedSelfCallTransformer.kt
new file mode 100644
index 0000000..6eb83bb
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainedSelfCallTransformer.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2025 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.powerassert
+
+import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+import org.jetbrains.kotlin.ir.declarations.IrValueParameter
+import org.jetbrains.kotlin.ir.expressions.IrCall
+import org.jetbrains.kotlin.ir.expressions.IrExpression
+import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
+import org.jetbrains.kotlin.ir.util.irCall
+
+/**
+ * Replaces all calls within a function to itself or super to include explanation parameter,
+ * so explanation is propagated to super function calls and recursive calls.
+ */
+class ExplainedSelfCallTransformer(
+    private val originalFunction: IrSimpleFunction,
+    private val explanation: IrValueParameter,
+) : IrElementTransformerVoidWithContext() {
+    override fun visitCall(expression: IrCall): IrExpression {
+        val call = if (expression.symbol == originalFunction.symbol || expression.symbol in originalFunction.overriddenSymbols) {
+            val explainedDispatchSymbol = expression.symbol.owner.explainedDispatchSymbol!!
+            irCall(expression, explainedDispatchSymbol, newSuperQualifierSymbol = expression.superQualifierSymbol).apply {
+                arguments[explainedDispatchSymbol.owner.parameters.last()] =
+                    IrGetValueImpl(expression.startOffset, expression.endOffset, explanation.type, explanation.symbol)
+            }
+        } else {
+            expression
+        }
+
+        call.transformChildren(this, null)
+        return call
+    }
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt
index 5b30cec..191ab39 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt
@@ -29,18 +29,21 @@
 import org.jetbrains.kotlin.ir.builders.irBlockBody
 import org.jetbrains.kotlin.ir.builders.parent
 import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
-import org.jetbrains.kotlin.ir.expressions.IrCall
 import org.jetbrains.kotlin.ir.declarations.IrParameterKind
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
 import org.jetbrains.kotlin.ir.expressions.*
 import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
-import org.jetbrains.kotlin.ir.expressions.isComparisonOperator
 import org.jetbrains.kotlin.ir.irAttribute
+import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
 import org.jetbrains.kotlin.ir.types.IrType
-import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
+import org.jetbrains.kotlin.ir.util.hasAnnotation
 import org.jetbrains.kotlin.ir.visitors.IrVisitorVoid
 import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
 import org.jetbrains.kotlin.name.Name
 
+fun IrSimpleFunction.hasAnnotationOrOverridden(annotation: IrClassSymbol): Boolean =
+    hasAnnotation(annotation) || overriddenSymbols.any { it.owner.hasAnnotationOrOverridden(annotation) }
+
 fun IrBuilderWithScope.irLambda(
     returnType: IrType,
     lambdaType: IrType,
@@ -87,7 +90,7 @@
 
         var range = startOffset..endOffset
         acceptChildrenVoid(
-            object : IrElementVisitorVoid {
+            object : IrVisitorVoid() {
                 override fun visitElement(element: IrElement) {
                     val childRange = element.sourceRange
                     range = minOf(range.start, childRange.start)..maxOf(range.endInclusive, childRange.endInclusive)
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt
index 4ad6b74..50f808e 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt
@@ -17,7 +17,7 @@
 val FUNCTION_FOR_EXPLAIN_CALL by IrDeclarationOriginImpl.Synthetic
 val EXPLANATION by IrDeclarationOriginImpl.Synthetic
 
-val PowerAssertGetDiagram = FqName("kotlin.explain.ExplainCall.Companion.<get-explanation>")
+val ExplainCallGetExplanation = FqName("kotlin.explain.ExplainCall.Companion.<get-explanation>")
 
 fun IrValueDeclaration.isExplained(): Boolean {
     val variable = this as? IrVariable ?: return false
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt
index b6372fe..d5e88a6 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt
@@ -24,6 +24,8 @@
     private val context: IrPluginContext,
 ) {
     companion object {
+        const val PLUGIN_ID = "org.jetbrains.kotlin.powerassert"
+
         private fun dependencyError(): Nothing {
             error("Power-Assert plugin runtime dependency was not found.")
         }
@@ -55,6 +57,8 @@
         private val callExplanationClassId = ClassId.topLevel(callExplanationFqName)
     }
 
+    val metadata = PowerAssertMetadata(context.languageVersionSettings.languageVersion)
+
     private fun referenceClass(classId: ClassId): IrClassSymbol =
         context.referenceClass(classId) ?: dependencyError()
 
@@ -69,6 +73,7 @@
 
     val explainCallClass = referenceClass(explainCallClassId)
     val explainCallType = explainCallClass.defaultTypeWithoutArguments
+    val explainCallConstructor = explainCallClass.primaryConstructor()
 
     val expressionClass = referenceClass(classId("Expression"))
     val expressionType = expressionClass.defaultTypeWithoutArguments
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt
index 5c54907..0b04e09 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt
@@ -37,7 +37,6 @@
 import org.jetbrains.kotlin.ir.builders.irSet
 import org.jetbrains.kotlin.ir.declarations.*
 import org.jetbrains.kotlin.ir.expressions.IrBlockBody
-import org.jetbrains.kotlin.ir.declarations.IrParameterKind
 import org.jetbrains.kotlin.ir.expressions.IrCall
 import org.jetbrains.kotlin.ir.expressions.IrExpression
 import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
@@ -82,8 +81,8 @@
         val scope = currentScope as PowerAssertScope
         val body = declaration.body
         if (body is IrBlockBody) {
-            for (variable in scope.variables.values.reversed()) {
-                body.statements.add(0, variable)
+            if (scope.variables.values.isNotEmpty()) {
+                body.statements.addAll(0, scope.variables.values)
             }
         }
 
@@ -122,11 +121,17 @@
     override fun visitCall(expression: IrCall): IrExpression {
         expression.transformChildrenVoid()
 
+        // Never transform calls within functions annotated with ExplainCall.
+        // TODO needs a better checks
+        if ((currentFunction?.irElement as? IrSimpleFunction)?.hasAnnotationOrOverridden(builtIns.explainCallClass) == true) {
+            return expression
+        }
+
         val function = expression.symbol.owner
         val fqName = function.kotlinFqName
         return when {
             function.parameters.isEmpty() -> expression
-            function.hasAnnotation(builtIns.explainCallClass) -> buildForAnnotated(expression, function)
+            function.hasAnnotationOrOverridden(builtIns.explainCallClass) -> buildForAnnotated(expression, function)
             fqName in configuration.functions -> buildForOverride(expression, function)
             else -> expression
         }
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertFunctionTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertFunctionTransformer.kt
deleted file mode 100644
index b9d42d3..0000000
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertFunctionTransformer.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.powerassert
-
-import org.jetbrains.kotlin.backend.common.DeclarationTransformer
-import org.jetbrains.kotlin.ir.declarations.IrDeclaration
-import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
-import org.jetbrains.kotlin.ir.util.hasAnnotation
-
-class PowerAssertFunctionTransformer(
-    private val builtIns: PowerAssertBuiltIns,
-    private val factory: ExplainCallFunctionFactory,
-) : DeclarationTransformer {
-    override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
-        if (declaration is IrSimpleFunction) {
-            if (declaration.hasAnnotation(builtIns.explainCallClass)) {
-                return lower(declaration)
-            }
-        }
-
-        return null
-    }
-
-    private fun lower(irFunction: IrSimpleFunction): List<IrSimpleFunction> {
-        return listOf(irFunction, factory.generate(irFunction))
-    }
-}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt
index 792398b..d26ae9d 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt
@@ -29,9 +29,9 @@
 ) : IrGenerationExtension {
     override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
         val builtIns = PowerAssertBuiltIns(pluginContext)
-        val factory = ExplainCallFunctionFactory(moduleFragment, pluginContext, builtIns)
+        val factory = ExplainCallFunctionFactory(pluginContext, builtIns)
 
-        val functionTransformer = PowerAssertFunctionTransformer(builtIns, factory)
+        val functionTransformer = ExplainCallFunctionTransformer(builtIns, factory)
         moduleFragment.files.forEach(functionTransformer::lower)
 
         for (file in moduleFragment.files) {
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertMetadata.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertMetadata.kt
new file mode 100644
index 0000000..7b97c2f
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertMetadata.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2010-2025 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.powerassert
+
+import org.jetbrains.kotlin.config.LanguageVersion
+
+/**
+ * Power-Assert metadata that helps track what version of the compiler was used to generate the
+ * `$explained` function overload of the original function. While the version is not currently used
+ * for anything, the presence of such metadata indicates that the explained function has been
+ * generated. In the future it may indicate what shape the explained function takes, for example,
+ * the parameter type of the explanation variable.
+ */
+@JvmInline
+value class PowerAssertMetadata(val data: ByteArray) {
+    constructor(version: LanguageVersion) : this(byteArrayOf(version.major.toByte(), version.minor.toByte()))
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt b/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt
index 43fd07c..3997401 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt
@@ -25,7 +25,7 @@
 import org.jetbrains.kotlin.config.CompilerConfiguration
 
 class PowerAssertCommandLineProcessor : CommandLineProcessor {
-    override val pluginId: String = "org.jetbrains.kotlin.powerassert"
+    override val pluginId: String = PowerAssertBuiltIns.PLUGIN_ID
 
     override val pluginOptions: Collection<CliOption> = listOf(
         CliOption(
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.box.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.box.txt
new file mode 100644
index 0000000..bf9680d
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.box.txt
@@ -0,0 +1,21 @@
+callTypeA: ---
+no description
+---
+callTypeB: ---
+type.describe(1 == 2)
+|               |
+TypeD           Expected <1>, actual <2>.
+
+---
+callTypeC: ---
+type.describe(1 == 2)
+|               |
+TypeD           Expected <1>, actual <2>.
+
+---
+callTypeD: ---
+type.describe(1 == 2)
+|               |
+TypeD           Expected <1>, actual <2>.
+
+---
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.fir.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.fir.kt.txt
new file mode 100644
index 0000000..0faba5e
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.fir.kt.txt
@@ -0,0 +1,153 @@
+// MODULE: lib
+// FILE: A.kt
+
+abstract class TypeB : TypeA {
+  constructor() /* primary */ {
+    super/*Any*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return { // BLOCK
+      val tmp_0: CallExplanation? = $explanation
+      when {
+        EQEQ(arg0 = tmp_0, arg1 = null) -> null
+        else -> tmp_0.toDefaultMessage()
+      }
+    }
+  }
+
+  @ExplainCall
+  override fun describe(value: Any): String? {
+    return { // BLOCK
+      val tmp_1: CallExplanation? = null
+      when {
+        EQEQ(arg0 = tmp_1, arg1 = null) -> null
+        else -> tmp_1.toDefaultMessage()
+      }
+    }
+  }
+
+}
+
+abstract class TypeC : TypeB {
+  constructor() /* primary */ {
+    super/*TypeB*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+  }
+
+  override fun describe(value: Any): String? {
+    return super<TypeB>.describe(value = value)
+  }
+
+}
+
+interface TypeA {
+  abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+  private constructor() /* primary */ {
+    super/*TypeC*/()
+    /* <init>() */
+
+  }
+
+  override operator fun equals(other: Any?): Boolean {
+    when {
+      EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+    }
+    when {
+      other !is TypeD -> return false
+    }
+    val tmp_2: TypeD = other as TypeD
+    return true
+  }
+
+  override fun hashCode(): Int {
+    return 81291306
+  }
+
+  override fun toString(): String {
+    return "TypeD"
+  }
+
+}
+
+// MODULE: main
+// FILE: B.kt
+
+fun box(): String {
+  return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+  val type: TypeA = TypeD
+  return { // BLOCK
+    val tmp_0: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+    when {
+      EQEQ(arg0 = tmp_0, arg1 = null) -> "no description"
+      else -> tmp_0
+    }
+  }
+}
+
+fun callTypeB(): String {
+  val type: TypeB = TypeD
+  return { // BLOCK
+    val tmp_1: String? = { // BLOCK
+      val tmp0_Explain: TypeB = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 392, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_1, arg1 = null) -> "no description"
+      else -> tmp_1
+    }
+  }
+}
+
+fun callTypeC(): String {
+  val type: TypeC = TypeD
+  return { // BLOCK
+    val tmp_2: String? = { // BLOCK
+      val tmp0_Explain: TypeC = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 502, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_2, arg1 = null) -> "no description"
+      else -> tmp_2
+    }
+  }
+}
+
+fun callTypeD(): String {
+  val type: TypeD = TypeD
+  return { // BLOCK
+    val tmp_3: String? = { // BLOCK
+      val tmp0_Explain: TypeD = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 612, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+      else -> tmp_3
+    }
+  }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt
new file mode 100644
index 0000000..5d9313e
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt
@@ -0,0 +1,56 @@
+// IGNORE_BACKEND_K1: ANY
+// DUMP_KT_IR
+
+// MODULE: lib
+// FILE: A.kt
+
+import kotlin.explain.*
+
+interface TypeA {
+    fun describe(value: Any): String?
+}
+
+abstract class TypeB : TypeA {
+    @ExplainCall // TODO should we even support this?
+    override fun describe(value: Any): String? {
+        return ExplainCall.explanation?.toDefaultMessage()
+    }
+}
+
+abstract class TypeC : TypeB() {
+    override fun describe(value: Any): String? {
+        return super.describe(value)
+    }
+}
+
+data object TypeD : TypeC()
+
+// MODULE: main(lib)
+// FILE: B.kt
+
+fun box(): String = runAllOutput(
+    "callTypeA" to ::callTypeA,
+    "callTypeB" to ::callTypeB,
+    "callTypeC" to ::callTypeC,
+    "callTypeD" to ::callTypeD,
+)
+
+fun callTypeA(): String {
+    val type: TypeA = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeB(): String {
+    val type: TypeB = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeC(): String {
+    val type: TypeC = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeD(): String {
+    val type: TypeD = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt.txt
new file mode 100644
index 0000000..cb325a4
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt.txt
@@ -0,0 +1,135 @@
+// MODULE: lib
+// FILE: A.kt
+
+abstract class TypeB : TypeA {
+  constructor() /* primary */ {
+    super/*Any*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return { // BLOCK
+      val tmp_0: CallExplanation? = $explanation
+      when {
+        EQEQ(arg0 = tmp_0, arg1 = null) -> null
+        else -> tmp_0.toDefaultMessage()
+      }
+    }
+  }
+
+  @ExplainCall
+  override fun describe(value: Any): String? {
+    return { // BLOCK
+      val tmp_1: CallExplanation? = null
+      when {
+        EQEQ(arg0 = tmp_1, arg1 = null) -> null
+        else -> tmp_1.toDefaultMessage()
+      }
+    }
+  }
+
+}
+
+abstract class TypeC : TypeB {
+  constructor() /* primary */ {
+    super/*TypeB*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+  }
+
+  override fun describe(value: Any): String? {
+    return super<TypeB>.describe(value = value)
+  }
+
+}
+
+interface TypeA {
+  abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+  private constructor() /* primary */ {
+    super/*TypeC*/()
+    /* <init>() */
+
+  }
+
+  override operator fun equals(other: Any?): Boolean {
+    when {
+      EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+    }
+    when {
+      other !is TypeD -> return false
+    }
+    val tmp_2: TypeD = other as TypeD
+    return true
+  }
+
+  override fun hashCode(): Int {
+    return 81291306
+  }
+
+  override fun toString(): String {
+    return "TypeD"
+  }
+
+}
+
+// MODULE: main
+// FILE: B.kt
+
+fun box(): String {
+  return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+  val type: TypeA = TypeD
+  return { // BLOCK
+    val tmp_0: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+    when {
+      EQEQ(arg0 = tmp_0, arg1 = null) -> "no description"
+      else -> tmp_0
+    }
+  }
+}
+
+fun callTypeB(): String {
+  val type: TypeB = TypeD
+  return { // BLOCK
+    val tmp_1: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+    when {
+      EQEQ(arg0 = tmp_1, arg1 = null) -> "no description"
+      else -> tmp_1
+    }
+  }
+}
+
+fun callTypeC(): String {
+  val type: TypeC = TypeD
+  return { // BLOCK
+    val tmp_2: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+    when {
+      EQEQ(arg0 = tmp_2, arg1 = null) -> "no description"
+      else -> tmp_2
+    }
+  }
+}
+
+fun callTypeD(): String {
+  val type: TypeD = TypeD
+  return { // BLOCK
+    val tmp_3: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+    when {
+      EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+      else -> tmp_3
+    }
+  }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.box.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.box.txt
new file mode 100644
index 0000000..bf9680d
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.box.txt
@@ -0,0 +1,21 @@
+callTypeA: ---
+no description
+---
+callTypeB: ---
+type.describe(1 == 2)
+|               |
+TypeD           Expected <1>, actual <2>.
+
+---
+callTypeC: ---
+type.describe(1 == 2)
+|               |
+TypeD           Expected <1>, actual <2>.
+
+---
+callTypeD: ---
+type.describe(1 == 2)
+|               |
+TypeD           Expected <1>, actual <2>.
+
+---
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.fir.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.fir.kt.txt
new file mode 100644
index 0000000..cddbb9a
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.fir.kt.txt
@@ -0,0 +1,147 @@
+abstract class TypeB : TypeA {
+  constructor() /* primary */ {
+    super/*Any*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return { // BLOCK
+      val tmp_0: CallExplanation? = $explanation
+      when {
+        EQEQ(arg0 = tmp_0, arg1 = null) -> null
+        else -> tmp_0.toDefaultMessage()
+      }
+    }
+  }
+
+  @ExplainCall
+  override fun describe(value: Any): String? {
+    return { // BLOCK
+      val tmp_1: CallExplanation? = null
+      when {
+        EQEQ(arg0 = tmp_1, arg1 = null) -> null
+        else -> tmp_1.toDefaultMessage()
+      }
+    }
+  }
+
+}
+
+abstract class TypeC : TypeB {
+  constructor() /* primary */ {
+    super/*TypeB*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+  }
+
+  override fun describe(value: Any): String? {
+    return super<TypeB>.describe(value = value)
+  }
+
+}
+
+interface TypeA {
+  abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+  private constructor() /* primary */ {
+    super/*TypeC*/()
+    /* <init>() */
+
+  }
+
+  override operator fun equals(other: Any?): Boolean {
+    when {
+      EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+    }
+    when {
+      other !is TypeD -> return false
+    }
+    val tmp_2: TypeD = other as TypeD
+    return true
+  }
+
+  override fun hashCode(): Int {
+    return 81291306
+  }
+
+  override fun toString(): String {
+    return "TypeD"
+  }
+
+}
+
+fun box(): String {
+  return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+  val type: TypeA = TypeD
+  return { // BLOCK
+    val tmp_3: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+    when {
+      EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+      else -> tmp_3
+    }
+  }
+}
+
+fun callTypeB(): String {
+  val type: TypeB = TypeD
+  return { // BLOCK
+    val tmp_4: String? = { // BLOCK
+      val tmp0_Explain: TypeB = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 750, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_4, arg1 = null) -> "no description"
+      else -> tmp_4
+    }
+  }
+}
+
+fun callTypeC(): String {
+  val type: TypeC = TypeD
+  return { // BLOCK
+    val tmp_5: String? = { // BLOCK
+      val tmp0_Explain: TypeC = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 860, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_5, arg1 = null) -> "no description"
+      else -> tmp_5
+    }
+  }
+}
+
+fun callTypeD(): String {
+  val type: TypeD = TypeD
+  return { // BLOCK
+    val tmp_6: String? = { // BLOCK
+      val tmp0_Explain: TypeD = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 970, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_6, arg1 = null) -> "no description"
+      else -> tmp_6
+    }
+  }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt
new file mode 100644
index 0000000..f07eb5d
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt
@@ -0,0 +1,49 @@
+// DUMP_KT_IR
+
+import kotlin.explain.*
+
+interface TypeA {
+    fun describe(value: Any): String?
+}
+
+abstract class TypeB : TypeA {
+    @ExplainCall // TODO should we even support this?
+    override fun describe(value: Any): String? {
+        return ExplainCall.explanation?.toDefaultMessage()
+    }
+}
+
+abstract class TypeC : TypeB() {
+    override fun describe(value: Any): String? {
+        return super.describe(value)
+    }
+}
+
+data object TypeD : TypeC()
+
+fun box(): String = runAllOutput(
+    "callTypeA" to ::callTypeA,
+    "callTypeB" to ::callTypeB,
+    "callTypeC" to ::callTypeC,
+    "callTypeD" to ::callTypeD,
+)
+
+fun callTypeA(): String {
+    val type: TypeA = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeB(): String {
+    val type: TypeB = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeC(): String {
+    val type: TypeC = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeD(): String {
+    val type: TypeD = TypeD
+    return type.describe(1 == 2) ?: "no description"
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt.txt
new file mode 100644
index 0000000..cddbb9a
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt.txt
@@ -0,0 +1,147 @@
+abstract class TypeB : TypeA {
+  constructor() /* primary */ {
+    super/*Any*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return { // BLOCK
+      val tmp_0: CallExplanation? = $explanation
+      when {
+        EQEQ(arg0 = tmp_0, arg1 = null) -> null
+        else -> tmp_0.toDefaultMessage()
+      }
+    }
+  }
+
+  @ExplainCall
+  override fun describe(value: Any): String? {
+    return { // BLOCK
+      val tmp_1: CallExplanation? = null
+      when {
+        EQEQ(arg0 = tmp_1, arg1 = null) -> null
+        else -> tmp_1.toDefaultMessage()
+      }
+    }
+  }
+
+}
+
+abstract class TypeC : TypeB {
+  constructor() /* primary */ {
+    super/*TypeB*/()
+    /* <init>() */
+
+  }
+
+  @JvmSynthetic
+  override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+    return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+  }
+
+  override fun describe(value: Any): String? {
+    return super<TypeB>.describe(value = value)
+  }
+
+}
+
+interface TypeA {
+  abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+  private constructor() /* primary */ {
+    super/*TypeC*/()
+    /* <init>() */
+
+  }
+
+  override operator fun equals(other: Any?): Boolean {
+    when {
+      EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+    }
+    when {
+      other !is TypeD -> return false
+    }
+    val tmp_2: TypeD = other as TypeD
+    return true
+  }
+
+  override fun hashCode(): Int {
+    return 81291306
+  }
+
+  override fun toString(): String {
+    return "TypeD"
+  }
+
+}
+
+fun box(): String {
+  return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+  val type: TypeA = TypeD
+  return { // BLOCK
+    val tmp_3: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+    when {
+      EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+      else -> tmp_3
+    }
+  }
+}
+
+fun callTypeB(): String {
+  val type: TypeB = TypeD
+  return { // BLOCK
+    val tmp_4: String? = { // BLOCK
+      val tmp0_Explain: TypeB = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 750, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_4, arg1 = null) -> "no description"
+      else -> tmp_4
+    }
+  }
+}
+
+fun callTypeC(): String {
+  val type: TypeC = TypeD
+  return { // BLOCK
+    val tmp_5: String? = { // BLOCK
+      val tmp0_Explain: TypeC = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 860, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_5, arg1 = null) -> "no description"
+      else -> tmp_5
+    }
+  }
+}
+
+fun callTypeD(): String {
+  val type: TypeD = TypeD
+  return { // BLOCK
+    val tmp_6: String? = { // BLOCK
+      val tmp0_Explain: TypeD = type
+      { // BLOCK
+        val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+        tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 970, source = "           type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+      }
+    }
+    when {
+      EQEQ(arg0 = tmp_6, arg1 = null) -> "no description"
+      else -> tmp_6
+    }
+  }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt
index 9a60318..9bef090 100644
--- a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt
@@ -34,7 +34,7 @@
       val tmp1_Explain: List<String> = tmp0_Explain.reversed<String>()
       val tmp2_Explain: List<String> = emptyList<String>()
       val tmp3_Explain: Boolean = EQEQ(arg0 = tmp1_Explain, arg1 = tmp2_Explain)
-      describe$explained(value = tmp3_Explain, $explanation = CallExplanation(offset = 110, source = "           describe(reallyLongList.reversed() == emptyList<String>())", dispatchReceiver = null, extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 20, endOffset = 68, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 20, endOffset = 34, displayOffset = 20, value = tmp0_Explain), ValueExpression(startOffset = 20, endOffset = 45, displayOffset = 35, value = tmp1_Explain), ValueExpression(startOffset = 49, endOffset = 68, displayOffset = 49, value = tmp2_Explain), EqualityExpression(startOffset = 20, endOffset = 68, displayOffset = 46, value = tmp3_Explain, lhs = tmp1_Explain, rhs = tmp2_Explain)])))])))
+      describe$explained(value = tmp3_Explain, $explanation = CallExplanation(offset = 111, source = "           describe(reallyLongList.reversed() == emptyList<String>())", dispatchReceiver = null, extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 20, endOffset = 68, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 20, endOffset = 34, displayOffset = 20, value = tmp0_Explain), ValueExpression(startOffset = 20, endOffset = 45, displayOffset = 35, value = tmp1_Explain), ValueExpression(startOffset = 49, endOffset = 68, displayOffset = 49, value = tmp2_Explain), EqualityExpression(startOffset = 20, endOffset = 68, displayOffset = 46, value = tmp3_Explain, lhs = tmp1_Explain, rhs = tmp2_Explain)])))])))
     }
     when {
       EQEQ(arg0 = tmp_0, arg1 = null) -> "FAIL"
@@ -42,4 +42,3 @@
     }
   }
 }
-
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt
index 4b310cf..ba4262a 100644
--- a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt
@@ -1,3 +1,4 @@
+// IGNORE_BACKEND_K1: ANY
 // DUMP_KT_IR
 
 // MODULE: lib
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt
index 9a60318..86ed7f8 100644
--- a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt
@@ -29,17 +29,10 @@
 fun box(): String {
   val reallyLongList: List<String> = listOf<String>(elements = ["a", "b"])
   return { // BLOCK
-    val tmp_0: String? = { // BLOCK
-      val tmp0_Explain: List<String> = reallyLongList
-      val tmp1_Explain: List<String> = tmp0_Explain.reversed<String>()
-      val tmp2_Explain: List<String> = emptyList<String>()
-      val tmp3_Explain: Boolean = EQEQ(arg0 = tmp1_Explain, arg1 = tmp2_Explain)
-      describe$explained(value = tmp3_Explain, $explanation = CallExplanation(offset = 110, source = "           describe(reallyLongList.reversed() == emptyList<String>())", dispatchReceiver = null, extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 20, endOffset = 68, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 20, endOffset = 34, displayOffset = 20, value = tmp0_Explain), ValueExpression(startOffset = 20, endOffset = 45, displayOffset = 35, value = tmp1_Explain), ValueExpression(startOffset = 49, endOffset = 68, displayOffset = 49, value = tmp2_Explain), EqualityExpression(startOffset = 20, endOffset = 68, displayOffset = 46, value = tmp3_Explain, lhs = tmp1_Explain, rhs = tmp2_Explain)])))])))
-    }
+    val tmp_0: String? = describe(value = EQEQ(arg0 = reallyLongList.reversed<String>(), arg1 = emptyList<String>()))
     when {
       EQEQ(arg0 = tmp_0, arg1 = null) -> "FAIL"
       else -> tmp_0
     }
   }
 }
-
diff --git a/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt b/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt
index 193f25a..9161a26 100644
--- a/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt
+++ b/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt
@@ -14,3 +14,19 @@
         "${name}: $msg"
     }
 }
+
+fun withThrowableMessage(block: () -> String): String {
+    val msg = try {
+        block()
+    } catch (e: Throwable) {
+        e.message ?: "no message"
+    }
+    return "---\n${msg}\n---\n"
+}
+
+fun runAllOutput(vararg tests: Pair<String, () -> String>): String {
+    return tests.joinToString("") { (name, func) ->
+        val msg = withThrowableMessage { func() }
+        "${name}: $msg"
+    }
+}
diff --git a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java
index 853ad11..0f78c44 100644
--- a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java
+++ b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * 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.
  */
 
@@ -101,10 +101,22 @@
     }
 
     @Test
+    @TestMetadata("ModuleOverride.kt")
+    public void testModuleOverride() {
+      runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt");
+    }
+
+    @Test
     @TestMetadata("modules.kt")
     public void testModules() {
       runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt");
     }
+
+    @Test
+    @TestMetadata("Override.kt")
+    public void testOverride() {
+      runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt");
+    }
   }
 
   @Nested
diff --git a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java
index 62edd09..c1a9002 100644
--- a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java
+++ b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * 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.
  */
 
@@ -101,10 +101,22 @@
     }
 
     @Test
+    @TestMetadata("ModuleOverride.kt")
+    public void testModuleOverride() {
+      runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt");
+    }
+
+    @Test
     @TestMetadata("modules.kt")
     public void testModules() {
       runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt");
     }
+
+    @Test
+    @TestMetadata("Override.kt")
+    public void testOverride() {
+      runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt");
+    }
   }
 
   @Nested