[FIR] Support including flow information when dumping CFG dot file

This commit is contained in:
Brian Norman
2023-11-14 07:47:12 -06:00
committed by Space Team
parent 6fbd26905a
commit e92fab65aa
9 changed files with 126 additions and 30 deletions
@@ -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("&", "&amp;")
.replace(">", "&gt;")
.replace("<", "&lt;")
}
@@ -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
View File
@@ -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 = ""
@@ -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,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
}