-
Notifications
You must be signed in to change notification settings - Fork 380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Control flow graph implementation #2687
Conversation
It would be nice to have tests, that covers Rust control flow construction. You can see example for java here and test data here |
@sirgl Good idea! What do you think about tests based on depth-first traversal traces? |
8b0b5fb
to
bf4de0c
Compare
Yes, I think, that any stable traversal is enough. |
} | ||
} | ||
|
||
class Node<N>(val data: N, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be formatted according to the convention.
import java.util.* | ||
|
||
class Graph<N, E>(val nodes: MutableList<Node<N>>, val edges: MutableList<Edge<E>>) { | ||
constructor() : this(mutableListOf(), mutableListOf()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to use default arguments.
preds.pop() | ||
assert(preds.size == oldPredsSize) | ||
|
||
return result!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe It's better to use checkNotNull
:
return checkNotNull(result) { "Optional (lazy) error message" }
This method throws IllegalStateException
instead of NullPointerException
.
|
||
private class ExitPointVisitor( | ||
private val sink: (ExitPoint) -> Unit | ||
) : RsVisitor() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it's better to rename all
o
insidevisit
methods to more understandable names.
56b50cb
to
6aef71b
Compare
f4f6fb9
to
6dfb899
Compare
val funcExit = process(func, pred) | ||
return straightLine(callExpr, funcExit, args) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why there is some space? If you want to separate some functions visually, I guess you better to leave a comment about it
} | ||
|
||
fun depthFirstTraversalTrace(): String = | ||
graph.depthFirstTraversal(this.entry).map { it.data.text }.joinToString("\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you should use graph.depthFirstTraversal(this.entry).joinToString("\n") { it.data.text }
} | ||
|
||
val nodesWithEntry = listOf(entryNode) + nodes | ||
nodesWithEntry.forEach { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use simple for
-loops when we can, as it is stated here
Direction.INCOMING -> incomingEdges(node) | ||
} | ||
|
||
fun forEachNode(f: (Node<N, E>) -> Unit) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those assignments are little misleading. Maybe you should convert those to bodies?
val block = exprStmt.parent as? RsBlock ?: return | ||
if (!(block.expr == null && block.stmtList.lastOrNull() == exprStmt)) return | ||
val parent = block.parent | ||
if ((parent is RsFunction || parent is RsExpr && parent.isInTailPosition) && exprStmt.expr.type != TyNever) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition seems heavy; maybe we should give it some name?
override fun visitExprStmt(exprStmt: RsExprStmt) { | ||
exprStmt.acceptChildren(this) | ||
val block = exprStmt.parent as? RsBlock ?: return | ||
if (!(block.expr == null && block.stmtList.lastOrNull() == exprStmt)) return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably this will be better looking like this:
if (block.expr != null) return
if (block.stmtList.lastOrNull() != exprStmt) return
class TailStatement(val stmt: RsExprStmt) : ExitPoint() | ||
|
||
companion object { | ||
fun process(fn: RsFunction, sink: (ExitPoint) -> Unit) = fn.block?.acceptChildren(ExitPointVisitor(sink)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should use ordinary body functions instead of this. You don't really need process
to return Unit?
* Creates graph description written in the DOT language. | ||
* Usage: copy the output into `cfg.dot` file and run `dot -Tpng cfg.dot -o cfg.png` | ||
*/ | ||
fun createDotDescription(): String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use buildString
:
/**
* Creates graph description written in the DOT language.
* Usage: copy the output into `cfg.dot` file and run `dot -Tpng cfg.dot -o cfg.png`
*/
fun createDotDescription(): String =
buildString {
append("digraph {\n")
graph.forEachEdge { edge ->
val source = edge.source
val target = edge.target
val sourceNode = source.data
val targetNode = target.data
append(" \"${source.index}: ${sourceNode.text}\" -> \"${target.index}: ${targetNode.text}\";\n")
}
append("}\n")
}
2f5cf4a
to
446db83
Compare
} | ||
} | ||
|
||
class CFGEdgeData(val exitingScopes: MutableList<RsElement>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why exitingScopes
is mutable? It doesn't have usages in the project (that means, at least, it isn't tested)
|
||
val cfgBuilder = CFGBuilder(graph, entry, fnExit) | ||
val bodyExit = cfgBuilder.process(body, entry) | ||
cfgBuilder.addContainedEdge(bodyExit, fnExit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like such a complex logic inside constructors and prefer constructors that only assign the fields (like rust's struct literals). Maybe we can move the logic to some factory method (like ControlFlowGraph.buildFor()
)
} | ||
|
||
fun addContainedEdge(source: CFGNode, target: CFGNode) { | ||
val data = CFGEdgeData(mutableListOf()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we really want a mutable list here? Maybe emptyList()
?
for (node in nodesWithEntry) { | ||
pushNode(node) | ||
while (stack.isNotEmpty()) { | ||
val (node, iter) = stack.pop() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name shadowing (compiler warning)
private fun addUnreachableNode(): CFGNode = | ||
addNode(CFGNodeData.Unreachable, emptyList()) | ||
|
||
private fun addNode(data: CFGNodeData, preds: List<CFGNode>): CFGNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found a lot of invocations of these methods with listOf()/emptyList() last argument. So maybe it's better to use vararg here?
private fun finishWithAstNode(element: RsElement, pred: CFGNode) = | ||
finishWith { addAstNode(element, listOf(pred)) } | ||
|
||
private fun withLoopScope(loopScope: LoopScope, callable: () -> Unit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is enough small method that accepts a closure. I think it's an obvious candidate to be inline
method.
preds.pop() | ||
assert(preds.size == oldPredsSize) | ||
|
||
return checkNotNull(result) { "Processing ended inconclusively" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we should set result to null after this. Otherwise, we can miss a bug and return a previous result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now result
sets to null
before element.accept(this)
.
import org.rust.lang.core.psi.RsBlock | ||
import org.rust.lang.core.psi.ext.descendantsOfType | ||
|
||
class RsControlFlowGraphTest : RsTestBase() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are very few tests. Especially, there are no tests for loop expression and loop expression with breaks. I.e. loop expr without breaks is really an infinite loop and should be the last node in CFG.
Try to run these tests with coverage and ensure all methods in CFGBuilder are executed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added more tests.
|
||
withLoopScope(loopScope) { | ||
val bodyExit = process(forExpr.block, loopBack) | ||
addContainedEdge(bodyExit, loopBack) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we processing for expression just like it is loop expression, that is incorrect.
override fun visitTryExpr(tryExpr: RsTryExpr) { | ||
val exprExit = addAstNode(tryExpr, emptyList()) | ||
val checkExpr = addDummyNode(listOf(pred)) | ||
val expr = addAstNode(tryExpr.expr, listOf(checkExpr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we have to process that expr too? Also, add some tests for try expr
d7537f0
to
3fd209d
Compare
|
||
typealias CFGGraph = Graph<CFGNodeData, CFGEdgeData> | ||
typealias CFGNode = Node<CFGNodeData, CFGEdgeData> | ||
typealias CFGEdge = Edge<CFGNodeData, CFGEdgeData> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CFGGraph & CFGEdge are not used anywhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got rid of CFGGraph
. CFGEdge
will be used in data-flow analysis.
} | ||
|
||
override fun visitPatBinding(patBinding: RsPatBinding) = | ||
finishWithAstNode(patBinding, pred) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this method is never executed. We process RsPatBinding in visitPatIdent
method (b/c RsPatBinding is a child of RsPatIdent)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really. It is used in the case of RsPatField
. But it's enough weird. Maybe it's better to override visitPatField
? and also please add a test for the "PatField" case.
bors r+ |
Build failed |
bors retry |
Build failed |
bors retry |
bors r=vlad20012 |
Build failed |
bors r=vlad20012 |
Build failed |
bors retry |
Rust control flow graph implementation. Will be used for future data flow analysis and borrow checker.