Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
eafa875
Try to improve identifying merge points in iterateEOG
KuechA Nov 12, 2025
7e00859
Connect the BB nodes with the graph
KuechA Nov 19, 2025
5b0a78e
Add scc class
KuechA Nov 19, 2025
7c5305c
Improve computation of region
KuechA Nov 21, 2025
5ea4f0b
Improve toString of basic block
KuechA Nov 21, 2025
bd0e810
Fix test
KuechA Nov 21, 2025
16717f9
Change SCC to an EOG edge property
KuechA Nov 21, 2025
b65fe6f
initial SCC support
morbitzer Nov 22, 2025
0c1537d
first version for support for nested SCCs
morbitzer Nov 22, 2025
51c3728
better version for support for nested SCCs + Test
morbitzer Nov 24, 2025
e2dbf57
iterateEOG: prioritize SCC Labeled EOG Edges
morbitzer Nov 25, 2025
c40781c
better handling of mergePoints
morbitzer Nov 27, 2025
a664df3
Merge branch 'main' into ak/try-to-fix-merge-points
morbitzer Nov 28, 2025
9612b1e
fixed most tests
morbitzer Nov 28, 2025
2070096
cleaned up
morbitzer Nov 28, 2025
7fafdc4
Fix neo4j test
KuechA Nov 28, 2025
001c285
basic block comments and refactoring
KuechA Nov 28, 2025
49bd9f9
Fix
KuechA Nov 28, 2025
d7babd1
Some refactoring of SccPass
KuechA Nov 28, 2025
a232311
added more complex EOG
morbitzer Dec 11, 2025
140cfb3
Merge branch 'main' into ak/try-to-fix-merge-points
morbitzer Dec 11, 2025
36196de
sccPass: consider single-BB SCCs
morbitzer Dec 12, 2025
abeceef
Add tests, ...
maximiliankaul Dec 15, 2025
bd30b9b
Merge remote-tracking branch 'origin/ak/try-to-fix-merge-points' into…
maximiliankaul Dec 15, 2025
528e757
Fix CDG for ShortCircuitOperators
maximiliankaul Dec 16, 2025
056011d
Merge branch 'main' into mk/issue2486
KuechA Jan 8, 2026
3263c6f
Some fixes
KuechA Jan 8, 2026
6d39a23
Minor cleanup
KuechA Jan 8, 2026
5c72300
Less code
KuechA Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,36 @@ operator fun Expression.minus(rhs: Expression): BinaryOperator {
return node
}

/**
* Creates a new [BinaryOperator] with a `&&` [BinaryOperator.operatorCode] in the Fluent Node DSL
* and invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder].
*/
context(frontend: LanguageFrontend<*, *>, holder: StatementHolder)
infix fun Expression.logicAnd(rhs: Expression): BinaryOperator {
val node = frontend.newBinaryOperator("&&")
node.lhs = this
node.rhs = rhs

holder += node

return node
}

/**
* Creates a new [BinaryOperator] with a `||` [BinaryOperator.operatorCode] in the Fluent Node DSL
* and invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder].
*/
context(frontend: LanguageFrontend<*, *>, holder: StatementHolder)
infix fun Expression.logicOr(rhs: Expression): BinaryOperator {
val node = frontend.newBinaryOperator("||")
node.lhs = this
node.rhs = rhs

holder += node

return node
}

/**
* Creates a new [UnaryOperator] with a `&` [UnaryOperator.operatorCode] in the Fluent Node DSL and
* invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,10 @@
package de.fraunhofer.aisec.cpg.graph.overlays

import de.fraunhofer.aisec.cpg.PopulatedByPass
import de.fraunhofer.aisec.cpg.graph.BranchingNode
import de.fraunhofer.aisec.cpg.graph.Node
import de.fraunhofer.aisec.cpg.graph.OverlayNode
import de.fraunhofer.aisec.cpg.graph.edges.overlay.BasicBlockEdgeList
import de.fraunhofer.aisec.cpg.graph.edges.unwrapping
import de.fraunhofer.aisec.cpg.graph.statements.LoopStatement
import de.fraunhofer.aisec.cpg.graph.statements.expressions.ComprehensionExpression
import de.fraunhofer.aisec.cpg.helpers.neo4j.LocationConverter
import de.fraunhofer.aisec.cpg.passes.BasicBlockCollectorPass
import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation
Expand Down Expand Up @@ -74,15 +71,12 @@ class BasicBlock() : OverlayNode() {
val endNode: Node?
get() = nodes.lastOrNull()

/**
* If this basic block ends in a branching point (i.e., has multiple outgoing EOG edges), this
* returns the [endNode] of this basic blocks EOG. Otherwise, null.
*/
val branchingNode: Node?
get() =
if (
endNode is BranchingNode ||
endNode is LoopStatement ||
endNode is ComprehensionExpression
) {
endNode as Node
} else null
get() = if (this.nextEOG.size > 1) endNode else null

@Convert(LocationConverter::class)
override var location: PhysicalLocation? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@
package de.fraunhofer.aisec.cpg.passes

import de.fraunhofer.aisec.cpg.TranslationContext
import de.fraunhofer.aisec.cpg.frontends.HasShortCircuitOperators
import de.fraunhofer.aisec.cpg.graph.EOGStarterHolder
import de.fraunhofer.aisec.cpg.graph.Node
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder
import de.fraunhofer.aisec.cpg.graph.overlays.BasicBlock
import de.fraunhofer.aisec.cpg.graph.statements.expressions.ShortCircuitOperator
import de.fraunhofer.aisec.cpg.passes.configuration.DependsOn
import java.util.*

@DependsOn(EvaluationOrderGraphPass::class)
class BasicBlockCollectorPass(ctx: TranslationContext) : EOGStarterPass(ctx) {
Expand All @@ -41,20 +44,26 @@ class BasicBlockCollectorPass(ctx: TranslationContext) : EOGStarterPass(ctx) {
}

override fun accept(t: Node) {
val firstBasicBlock = collectBasicBlocks(t)
val firstBasicBlock = collectBasicBlocks(t, t.language is HasShortCircuitOperators)
(t as? EOGStarterHolder)?.firstBasicBlock = firstBasicBlock
}

/**
* Collects all basic blocks starting at the given [startNode]. We identify basic blocks by
* iterating through the EOG in O(n) and collecting all nodes which are reachable from the
* [startNode].
* [startNode]. We identify basic blocks by merges and branches in the EOG but may not consider
* [ShortCircuitOperator]s as EOG-splitting nodes if we set [splitOnShortCircuitOperator] to
* `false`.
*
* It returns the first basic block, which is the one starting at the [startNode].
*
* @param startNode The node to start the collection from, usually a [FunctionDeclaration].
* @param splitOnShortCircuitOperator If true, the basic blocks will be split on short-circuit
* operators (e.g., `&&` or `||`). If false, the short-circuit operators will not be
* considered, and the basic blocks will be collected as if they were not splitting up the
* EOG.
*/
fun collectBasicBlocks(startNode: Node): BasicBlock {
fun collectBasicBlocks(startNode: Node, splitOnShortCircuitOperator: Boolean): BasicBlock {
val firstBB = BasicBlock()
val worklist =
mutableListOf<Triple<Node, EvaluationOrder?, BasicBlock>>(
Expand All @@ -71,7 +80,6 @@ class BasicBlockCollectorPass(ctx: TranslationContext) : EOGStarterPass(ctx) {
oldBB.prevEOG += basicBlock
continue
}
// We have not seen this node yet, so we can continue.

if (
currentStartNode.prevEOG.size > 1 &&
Expand All @@ -95,9 +103,30 @@ class BasicBlockCollectorPass(ctx: TranslationContext) : EOGStarterPass(ctx) {
}
}
}

basicBlock.nodes.add(currentStartNode)

val nextRelevantEOGEdges = currentStartNode.nextEOGEdges
// Note: This may not identify all cases correctly, e.g., if a ShortCircuitOperator is
// not the direct astParent.
val shortCircuit = currentStartNode.astParent as? ShortCircuitOperator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to make the look-ahead in the EOG here instead of checking the astParent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try that.

val language = shortCircuit?.language
val nextRelevantEOGEdges =
if (
shortCircuit != null &&
language is HasShortCircuitOperators &&
!splitOnShortCircuitOperator &&
currentStartNode.nextEOGEdges.size > 1
) {
// For ShortCircuitOperators, we select only the branch which is not a shortcut
// because it's not really a CDG-relevant node, and we want to save the branches
// it introduces.
currentStartNode.nextEOGEdges.filter {
shortCircuit.operatorCode in language.conjunctiveOperators == it.branch ||
it.branch == null
}
} else {
currentStartNode.nextEOGEdges
}

if (nextRelevantEOGEdges.size > 1) {
// If the currentStartNode splits up into multiple paths, the next nodes start a new
Expand Down Expand Up @@ -137,6 +166,7 @@ class BasicBlockCollectorPass(ctx: TranslationContext) : EOGStarterPass(ctx) {
// List is empty, nothing to do.
}
}

return firstBB
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
package de.fraunhofer.aisec.cpg.passes

import de.fraunhofer.aisec.cpg.TranslationContext
import de.fraunhofer.aisec.cpg.frontends.HasShortCircuitOperators
import de.fraunhofer.aisec.cpg.graph.BranchingNode
import de.fraunhofer.aisec.cpg.graph.EOGStarterHolder
import de.fraunhofer.aisec.cpg.graph.Node
Expand All @@ -44,8 +45,6 @@ import de.fraunhofer.aisec.cpg.helpers.functional.Lattice
import de.fraunhofer.aisec.cpg.helpers.functional.MapLattice
import de.fraunhofer.aisec.cpg.helpers.functional.PowersetLattice
import de.fraunhofer.aisec.cpg.passes.configuration.DependsOn
import kotlin.collections.component1
import kotlin.collections.component2

/** This pass builds the Control Dependence Graph (CDG) by iterating through the EOG. */
@DependsOn(EvaluationOrderGraphPass::class)
Expand Down Expand Up @@ -102,7 +101,8 @@ open class ControlDependenceGraphPass(ctx: TranslationContext) : EOGStarterPass(

val firstBasicBlock =
(startNode as? EOGStarterHolder)?.firstBasicBlock
?: BasicBlockCollectorPass(ctx).collectBasicBlocks(startNode)
?: BasicBlockCollectorPass(ctx)
.collectBasicBlocks(startNode, startNode.language is HasShortCircuitOperators)

log.trace("Retrieved network of BBs for {}", startNode.name)

Expand Down Expand Up @@ -181,7 +181,7 @@ open class ControlDependenceGraphPass(ctx: TranslationContext) : EOGStarterPass(
}
}
.flatten()
finalDominators = finalDominators.minus(transitiveDominators).toMutableList()
finalDominators = finalDominators.minus(transitiveDominators.toSet()).toMutableList()

// After deleting a bunch of stuff, we have two options: 1) there are no dominators
// left, and we assign the function declaration, or 2) there is one or multiple
Expand Down Expand Up @@ -349,8 +349,14 @@ private fun EvaluationOrder.isConditionalBranch(): Boolean {
startNode is ComprehensionExpression ||
(startNode.astParent is ComprehensionExpression &&
startNode == (startNode.astParent as ComprehensionExpression).iterable) ||
startNode is ConditionalExpression ||
startNode is ShortCircuitOperator) && branch == false ||
startNode is ConditionalExpression) && branch == false ||
/*
* Code like `foo() && bar()` requires us to look-ahead for a [ShortCircuitOperator].
* The execution of the rhs of the [ShortCircuitOperator] always depends on the lhs:
* `foo() && bar()` -> `bar()` will only be called if `foo()` evaluates to `true`
* `foo() || bar()` -> `bar()` will only be called if `foo() evaluates to `false`
*/
startNode.nextEOG.filterIsInstance<ShortCircuitOperator>().isNotEmpty() ||
(startNode is IfStatement &&
!startNode.allBranchesFromMyThenBranchGoThrough(startNode.nextUnconditionalNode))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ package de.fraunhofer.aisec.cpg.passes

import de.fraunhofer.aisec.cpg.TranslationConfiguration
import de.fraunhofer.aisec.cpg.frontends.TestLanguageWithColon
import de.fraunhofer.aisec.cpg.frontends.TestLanguageWithShortCircuit
import de.fraunhofer.aisec.cpg.frontends.testFrontend
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.builder.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.assertNotNull

class ControlDependenceGraphPassTest {

@Test
fun testIfStatements() {
val result = getIfTest()
Expand Down Expand Up @@ -112,12 +112,73 @@ class ControlDependenceGraphPassTest {
}

@Test
fun testTimoutEffective() {
fun testTimeoutEffective() {
val result = getTimeoutTest()
assertTrue { result.allChildren<Node>().flatMap { it.nextCDG }.isEmpty() }
}

@Test
fun testShortCircuit() {
val result = getShortCircuitTest()
assertNotNull(result)

val barCall = result.calls("bar").singleOrNull()
assertNotNull(barCall)
val fooCall = result.calls("foo").singleOrNull()
assertNotNull(fooCall)
val bazCall = result.calls("baz").singleOrNull()
assertNotNull(bazCall)
val quuxCall = result.calls("quux").singleOrNull()
assertNotNull(quuxCall)
assertTrue(
barCall.prevCDG.contains(fooCall),
"Expected 'bar()' to be control dependent on 'foo()'",
) // TODO: Once we update the ShortCircuitOperator to a better EOG description, this should
// test against the operator instead of foo().

assertTrue(
quuxCall.prevCDG.contains(bazCall),
"Expected 'quux()' to be control dependent on 'baz()'",
) // TODO: Once we update the ShortCircuitOperator to a better EOG description, this should
// test against the operator instead of baz().
}

companion object {

/**
* Test language with [HasShortCircuitOperators] to test short-circuit behavior in CDG.
*
* ```c
* int main() {
* foo() && bar();
* baz() || quux();
* return 1;
* }
* ```
*/
fun getShortCircuitTest() =
testFrontend(
TranslationConfiguration.builder()
.registerLanguage<TestLanguageWithShortCircuit>()
.defaultPasses()
.registerPass<ControlDependenceGraphPass>()
.build()
)
.build {
translationResult {
translationUnit("if.cpp") {
// The main method
function("main", t("int")) {
body {
call("foo") logicAnd call("bar")
call("baz") logicOr call("quux")
returnStmt { literal(1, t("int")) }
}
}
}
}
}

fun getIfTest() =
testFrontend(
TranslationConfiguration.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
*/
package de.fraunhofer.aisec.cpg.frontends

import de.fraunhofer.aisec.cpg.*
import de.fraunhofer.aisec.cpg.TranslationConfiguration
import de.fraunhofer.aisec.cpg.TranslationContext
import de.fraunhofer.aisec.cpg.graph.Node
import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration
Expand All @@ -36,6 +37,12 @@ import java.io.File
import kotlin.reflect.KClass
import kotlin.test.assertNotNull

/** This is a variant of the test language with [HasShortCircuitOperators]. */
open class TestLanguageWithShortCircuit() : TestLanguage(), HasShortCircuitOperators {
override val conjunctiveOperators: List<String> = listOf("&&")
override val disjunctiveOperators: List<String> = listOf("||")
}

/**
* This is a variant of the test language with `::` as a [namespaceDelimiter] to simulate languages
* like C++.
Expand Down
Loading