[FIR] Support including flow information when dumping CFG dot file
This commit is contained in:
+100
-8
@@ -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<String>()
|
||||
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("<TABLE BORDER=\"0\">")
|
||||
append("<TR><TD><B>")
|
||||
append(node.render().toHtmlLikeString())
|
||||
if (renderLevels) {
|
||||
append(" [${node.level}]")
|
||||
}
|
||||
append("</B></TD></TR>")
|
||||
if (node.flowInitialized) {
|
||||
append("<TR><TD ALIGN=\"LEFT\" BALIGN=\"LEFT\">")
|
||||
append(node.renderFlowHtmlLike())
|
||||
append("</TD></TR>")
|
||||
}
|
||||
append("</TABLE>")
|
||||
}
|
||||
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 = "<BR/><BR/>") { variable ->
|
||||
buildString {
|
||||
append(variable.renderHtmlLike())
|
||||
if (variable is RealVariable) {
|
||||
flow.getTypeStatement(variable)?.let {
|
||||
append("<BR/><B>types</B> ")
|
||||
append(it.exactType.toHtmlLikeString())
|
||||
}
|
||||
flow.implications[flow.unwrapVariable(variable)]?.let {
|
||||
for (implication in it) {
|
||||
append("<BR/><B>implication</B> ")
|
||||
append(implication.toHtmlLikeString())
|
||||
}
|
||||
}
|
||||
}
|
||||
flow.implications[variable]?.let {
|
||||
for (implication in it) {
|
||||
append("<BR/><B>implication</B> ")
|
||||
append(implication.toHtmlLikeString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DataFlowVariable.renderHtmlLike(): String {
|
||||
val variable = this
|
||||
return buildString {
|
||||
append("<B>")
|
||||
append(variable)
|
||||
append("</B>")
|
||||
|
||||
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("<", "<")
|
||||
}
|
||||
|
||||
@@ -22,10 +22,12 @@ data class Identifier(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DataFlowVariable(private val variableIndexForDebug: Int) {
|
||||
sealed class DataFlowVariable(private val variableIndexForDebug: Int) : Comparable<DataFlowVariable> {
|
||||
final override fun toString(): String {
|
||||
return "d$variableIndexForDebug"
|
||||
}
|
||||
|
||||
override fun compareTo(other: DataFlowVariable): Int = variableIndexForDebug.compareTo(other.variableIndexForDebug)
|
||||
}
|
||||
|
||||
enum class PropertyStability(val impliedSmartcastStability: SmartcastStability?) {
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
// ISSUE: KT-44814
|
||||
// WITH_STDLIB
|
||||
// DUMP_IR
|
||||
// DUMP_CFG
|
||||
// RENDERER_CFG_LEVELS
|
||||
// DUMP_CFG: LEVELS
|
||||
|
||||
class FlyweightCapableTreeStructure
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIR_IDENTICAL
|
||||
// DUMP_CFG
|
||||
// RENDERER_CFG_LEVELS
|
||||
// DUMP_CFG: LEVELS
|
||||
// !DIAGNOSICS: +UNUSED_PARAMETER
|
||||
|
||||
fun foo(x: Int) = 1
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
+12
-8
@@ -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
|
||||
|
||||
+6
-3
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user