diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/ControlFlowGraphRenderer.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/ControlFlowGraphRenderer.kt index ae60d06b18a..04b45615edf 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/ControlFlowGraphRenderer.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/ControlFlowGraphRenderer.kt @@ -10,15 +10,18 @@ package org.jetbrains.kotlin.fir.resolve.dfa.cfg import org.jetbrains.kotlin.fir.FirElement import org.jetbrains.kotlin.fir.declarations.FirFile import org.jetbrains.kotlin.fir.references.FirControlFlowGraphReference -import org.jetbrains.kotlin.fir.resolve.dfa.FirControlFlowGraphReferenceImpl +import org.jetbrains.kotlin.fir.render +import org.jetbrains.kotlin.fir.resolve.dfa.* +import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.utils.DFS import org.jetbrains.kotlin.utils.Printer -import java.util.* class FirControlFlowGraphRenderVisitor( builder: StringBuilder, - private val renderLevels: Boolean = false + private val renderLevels: Boolean = false, + private val renderFlow: Boolean = false, ) : FirVisitorVoid() { companion object { private const val EDGE = " -> " @@ -59,13 +62,33 @@ class FirControlFlowGraphRenderVisitor( color = BLUE } val attributes = mutableListOf() - val label = buildString { - append(node.render().replace("\"", "")) - if (renderLevels) { - append(" [${node.level}]") + if (renderFlow) { + // To maintain compatibility with existing CFG renders, only use HTML-like rendering if flow details are enabled. + val label = buildString { + append("") + append("") + if (node.flowInitialized) { + append("") + } + append("
") + append(node.render().toHtmlLikeString()) + if (renderLevels) { + append(" [${node.level}]") + } + append("
") + append(node.renderFlowHtmlLike()) + append("
") } + attributes += "label=< $label >" + } else { + val label = buildString { + append(node.render().replace("\"", "")) + if (renderLevels) { + append(" [${node.level}]") + } + } + attributes += "label=\"$label\"" } - attributes += "label=\"$label\"" when { node.isDead -> "gray" @@ -142,4 +165,73 @@ class FirControlFlowGraphRenderVisitor( popIndent() println("}") } + + private fun CFGNode<*>.renderFlowHtmlLike(): String { + val flow = flow + val variables = flow.knownVariables + flow.implications.keys + + flow.implications.flatMap { it.value }.map { it.condition.variable } + + flow.implications.flatMap { it.value }.map { it.effect.variable } + return variables.sorted().joinToString(separator = "

") { variable -> + buildString { + append(variable.renderHtmlLike()) + if (variable is RealVariable) { + flow.getTypeStatement(variable)?.let { + append("
types ") + append(it.exactType.toHtmlLikeString()) + } + flow.implications[flow.unwrapVariable(variable)]?.let { + for (implication in it) { + append("
implication ") + append(implication.toHtmlLikeString()) + } + } + } + flow.implications[variable]?.let { + for (implication in it) { + append("
implication ") + append(implication.toHtmlLikeString()) + } + } + } + } + } + + private fun DataFlowVariable.renderHtmlLike(): String { + val variable = this + return buildString { + append("") + append(variable) + append("") + + val callableId = variable.callableId + if (variable is RealVariable && callableId != null) { + append(" = ") + val receivers = listOfNotNull( + variable.identifier.dispatchReceiver?.callableId?.toHtmlLikeString(), + variable.identifier.extensionReceiver?.callableId?.toHtmlLikeString(), + ) + when (receivers.size) { + 2 -> append(receivers.joinToString(prefix = "(", postfix = ").")) + 1 -> append(receivers.joinToString(postfix = ".")) + } + append(callableId.toHtmlLikeString()) + } + if (variable is SyntheticVariable) { + append(" = '") + append(variable.fir.render().toHtmlLikeString()) + append("'") + } + } + } + + private val DataFlowVariable.callableId: CallableId? + get() = ((this as? RealVariable)?.identifier?.symbol as? FirCallableSymbol<*>)?.callableId + + /** + * Sanitize string for rendering with HTML-like syntax. + */ + private fun Any.toHtmlLikeString(): String = toString() + .replace("&", "&") + .replace(">", ">") + .replace("<", "<") } diff --git a/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/DfaVariables.kt b/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/DfaVariables.kt index a0f219c0f4e..2b202127457 100644 --- a/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/DfaVariables.kt +++ b/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/DfaVariables.kt @@ -22,10 +22,12 @@ data class Identifier( } } -sealed class DataFlowVariable(private val variableIndexForDebug: Int) { +sealed class DataFlowVariable(private val variableIndexForDebug: Int) : Comparable { final override fun toString(): String { return "d$variableIndexForDebug" } + + override fun compareTo(other: DataFlowVariable): Int = variableIndexForDebug.compareTo(other.variableIndexForDebug) } enum class PropertyStability(val impliedSmartcastStability: SmartcastStability?) { diff --git a/compiler/testData/codegen/box/smartCasts/kt44814.kt b/compiler/testData/codegen/box/smartCasts/kt44814.kt index efab025c5a8..f3bb6cbb916 100644 --- a/compiler/testData/codegen/box/smartCasts/kt44814.kt +++ b/compiler/testData/codegen/box/smartCasts/kt44814.kt @@ -1,8 +1,7 @@ // ISSUE: KT-44814 // WITH_STDLIB // DUMP_IR -// DUMP_CFG -// RENDERER_CFG_LEVELS +// DUMP_CFG: LEVELS class FlyweightCapableTreeStructure diff --git a/compiler/testData/diagnostics/tests/script/ComplexScript.kts b/compiler/testData/diagnostics/tests/script/ComplexScript.kts index 62d5bfca1c6..7ee98cd700c 100644 --- a/compiler/testData/diagnostics/tests/script/ComplexScript.kts +++ b/compiler/testData/diagnostics/tests/script/ComplexScript.kts @@ -1,6 +1,5 @@ // FIR_IDENTICAL -// DUMP_CFG -// RENDERER_CFG_LEVELS +// DUMP_CFG: LEVELS // !DIAGNOSICS: +UNUSED_PARAMETER fun foo(x: Int) = 1 diff --git a/compiler/testData/diagnostics/tests/script/NestedInnerClass.fir.kts b/compiler/testData/diagnostics/tests/script/NestedInnerClass.fir.kts index 1a666e2054e..e2516359f0d 100644 --- a/compiler/testData/diagnostics/tests/script/NestedInnerClass.fir.kts +++ b/compiler/testData/diagnostics/tests/script/NestedInnerClass.fir.kts @@ -1,7 +1,6 @@ // !WITH_NEW_INFERENCE // documents inconsistency between scripts and classes, see DeclarationScopeProviderImpl -// DUMP_CFG -// RENDERER_CFG_LEVELS +// DUMP_CFG: LEVELS fun function() = 42 val property = "" diff --git a/compiler/testData/diagnostics/tests/script/NestedInnerClass.kts b/compiler/testData/diagnostics/tests/script/NestedInnerClass.kts index 8d1e448d421..e511ff7ef09 100644 --- a/compiler/testData/diagnostics/tests/script/NestedInnerClass.kts +++ b/compiler/testData/diagnostics/tests/script/NestedInnerClass.kts @@ -1,7 +1,6 @@ // !WITH_NEW_INFERENCE // documents inconsistency between scripts and classes, see DeclarationScopeProviderImpl -// DUMP_CFG -// RENDERER_CFG_LEVELS +// DUMP_CFG: LEVELS fun function() = 42 val property = "" diff --git a/compiler/testData/diagnostics/tests/script/NestedInnerClass.ll.kts b/compiler/testData/diagnostics/tests/script/NestedInnerClass.ll.kts index f6df3b9dd7a..7ed133a00e1 100644 --- a/compiler/testData/diagnostics/tests/script/NestedInnerClass.ll.kts +++ b/compiler/testData/diagnostics/tests/script/NestedInnerClass.ll.kts @@ -3,8 +3,7 @@ // LL_FIR_DIVERGENCE // !WITH_NEW_INFERENCE // documents inconsistency between scripts and classes, see DeclarationScopeProviderImpl -// DUMP_CFG -// RENDERER_CFG_LEVELS +// DUMP_CFG: LEVELS fun function() = 42 val property = "" diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/FirDiagnosticsDirectives.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/FirDiagnosticsDirectives.kt index 927806639e1..8bbec608af0 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/FirDiagnosticsDirectives.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/FirDiagnosticsDirectives.kt @@ -14,17 +14,16 @@ import org.jetbrains.kotlin.test.frontend.fir.handlers.FirResolvedTypesVerifier import org.jetbrains.kotlin.test.frontend.fir.handlers.FirScopeDumpHandler object FirDiagnosticsDirectives : SimpleDirectivesContainer() { - val DUMP_CFG by directive( + val DUMP_CFG by stringDirective( description = """ Dumps control flow graphs of all declarations to `testName.dot` file - This directive may be applied only to all modules + This directive may be applied only to all modules. + Syntax: DUMP_CFG(: [OPTIONS]) + + Additional options may be enabled : + - ${DumpCfgOption.LEVELS}: Render levels of nodes in CFG dump. + - ${DumpCfgOption.FLOW}: Include data analysis variable information in CFG dump for debugging purposes. """.trimIndent(), - applicability = Global - ) - - val RENDERER_CFG_LEVELS by directive( - description = "Render leves of nodes in CFG dump", - applicability = Global ) val FIR_DUMP by directive( @@ -96,6 +95,11 @@ object FirDiagnosticsDirectives : SimpleDirectivesContainer() { ) } +object DumpCfgOption { + const val LEVELS = "LEVELS" + const val FLOW = "FLOW" +} + fun TestConfigurationBuilder.configureFirParser(parser: FirParser) { defaultDirectives { FIR_PARSER with parser diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/frontend/fir/handlers/FirCfgDumpHandler.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/frontend/fir/handlers/FirCfgDumpHandler.kt index ef893557abb..5fd97f99fb2 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/frontend/fir/handlers/FirCfgDumpHandler.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/frontend/fir/handlers/FirCfgDumpHandler.kt @@ -6,8 +6,8 @@ package org.jetbrains.kotlin.test.frontend.fir.handlers import org.jetbrains.kotlin.fir.resolve.dfa.cfg.FirControlFlowGraphRenderVisitor +import org.jetbrains.kotlin.test.directives.DumpCfgOption import org.jetbrains.kotlin.test.directives.FirDiagnosticsDirectives -import org.jetbrains.kotlin.test.directives.FirDiagnosticsDirectives.RENDERER_CFG_LEVELS import org.jetbrains.kotlin.test.directives.model.DirectivesContainer import org.jetbrains.kotlin.test.frontend.fir.FirOutputArtifact import org.jetbrains.kotlin.test.model.TestModule @@ -24,9 +24,12 @@ class FirCfgDumpHandler(testServices: TestServices) : FirAnalysisHandler(testSer override fun processModule(module: TestModule, info: FirOutputArtifact) { if (alreadyDumped || FirDiagnosticsDirectives.DUMP_CFG !in module.directives) return + val options = module.directives[FirDiagnosticsDirectives.DUMP_CFG].map { it.uppercase() } + val file = info.mainFirFiles.values.first() - val renderLevels = RENDERER_CFG_LEVELS in module.directives - file.accept(FirControlFlowGraphRenderVisitor(builder, renderLevels)) + val renderLevels = DumpCfgOption.LEVELS in options + val renderFlow = DumpCfgOption.FLOW in options + file.accept(FirControlFlowGraphRenderVisitor(builder, renderLevels, renderFlow)) alreadyDumped = true }