Skip to content

Commit

Permalink
Add CFG DSL (#294)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lipen authored Feb 5, 2025
1 parent deeaae6 commit d03090c
Show file tree
Hide file tree
Showing 18 changed files with 1,517 additions and 5 deletions.
174 changes: 174 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dsl/BlockCfg.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jacodb.ets.dsl

import org.jacodb.ets.utils.IdentityHashSet
import java.util.IdentityHashMap

data class Block(
val id: Int,
val statements: List<BlockStmt>,
)

data class BlockCfg(
val blocks: List<Block>,
val successors: Map<Int, List<Int>>,
)

fun Program.toBlockCfg(): BlockCfg {
val labelToNode: MutableMap<String, Node> = hashMapOf()
val targets: MutableSet<Node> = IdentityHashSet()

fun findLabels(nodes: List<Node>) {
if (nodes.lastOrNull() is Label) {
error("Label at the end of the block: $nodes")
}
for ((stmt, next) in nodes.zipWithNext()) {
if (stmt is Label) {
check(next !is Label) { "Two labels in a row: $stmt, $next" }
check(next !is Goto) { "Label followed by goto: $stmt, $next" }
check(stmt.name !in labelToNode) { "Duplicate label: ${stmt.name}" }
labelToNode[stmt.name] = next
}
}
for (node in nodes) {
if (node is If) {
findLabels(node.thenBranch)
findLabels(node.elseBranch)
}
if (node is Goto) {
targets += labelToNode[node.targetLabel] ?: error("Unknown label: ${node.targetLabel}")
}
}
}

findLabels(nodes)

val blocks: MutableList<Block> = mutableListOf()
val successors: MutableMap<Int, List<Int>> = hashMapOf()
val stmtToBlock: MutableMap<BlockStmt, Int> = IdentityHashMap()
val nodeToStmt: MutableMap<Node, BlockStmt> = IdentityHashMap()

fun buildBlocks(nodes: List<Node>): Pair<Int, Int>? {
if (nodes.isEmpty()) return null

lateinit var currentBlock: MutableList<BlockStmt>

fun newBlock(): Block {
currentBlock = mutableListOf()
val block = Block(blocks.size, currentBlock)
blocks += block
return block
}

var block = newBlock()
val firstBlockId = block.id

for (node in nodes) {
if (node is Label) continue

if (node in targets && currentBlock.isNotEmpty()) {
block.statements.forEach { stmtToBlock[it] = block.id }
val prevBlock = block
block = newBlock()
successors[prevBlock.id] = listOf(block.id)
}

if (node !is Goto) {
val stmt = when (node) {
Nop -> BlockNop
is Assign -> BlockAssign(node.target, node.expr)
is Return -> BlockReturn(node.expr)
is If -> BlockIf(node.condition)
else -> error("Unexpected node: $node")
}
nodeToStmt[node] = stmt
currentBlock += stmt
}

if (node is If) {
block.statements.forEach { stmtToBlock[it] = block.id }
val ifBlock = block
block = newBlock()

val thenBlocks = buildBlocks(node.thenBranch)
val elseBlocks = buildBlocks(node.elseBranch)

when {
thenBlocks != null && elseBlocks != null -> {
val (thenStart, thenEnd) = thenBlocks
val (elseStart, elseEnd) = elseBlocks
successors[ifBlock.id] = listOf(thenStart, elseStart) // (true, false) branches
when (blocks[thenEnd].statements.lastOrNull()) {
is BlockReturn -> {}
is BlockIf -> error("Unexpected if statement at the end of the block")
else -> successors[thenEnd] = listOf(block.id)
}
when (blocks[elseEnd].statements.lastOrNull()) {
is BlockReturn -> {}
is BlockIf -> error("Unexpected if statement at the end of the block")
else -> successors[elseEnd] = listOf(block.id)
}
}

thenBlocks != null -> {
val (thenStart, thenEnd) = thenBlocks
successors[ifBlock.id] = listOf(thenStart, block.id) // (true, false) branches
when (blocks[thenEnd].statements.lastOrNull()) {
is BlockReturn -> {}
is BlockIf -> error("Unexpected if statement at the end of the block")
else -> successors[thenEnd] = listOf(block.id)
}
}

elseBlocks != null -> {
val (elseStart, elseEnd) = elseBlocks
successors[ifBlock.id] = listOf(block.id, elseStart) // (true, false) branches
when (blocks[elseEnd].statements.lastOrNull()) {
is BlockReturn -> {}
is BlockIf -> error("Unexpected if statement at the end of the block")
else -> successors[elseEnd] = listOf(block.id)
}
}

else -> {
successors[ifBlock.id] = listOf(block.id)
}
}
} else if (node is Goto) {
val targetNode = labelToNode[node.targetLabel] ?: error("Unknown label: ${node.targetLabel}")
val target = nodeToStmt[targetNode] ?: error("No statement for $targetNode")
val targetBlockId = stmtToBlock[target] ?: error("No block for $target")
successors[block.id] = listOf(targetBlockId)
block.statements.forEach { stmtToBlock[it] = block.id }
block = newBlock()
} else if (node is Return) {
successors[block.id] = emptyList()
break
}
}

block.statements.forEach { stmtToBlock[it] = block.id }
val lastBlockId = block.id

return Pair(firstBlockId, lastBlockId)
}

buildBlocks(nodes)

return BlockCfg(blocks, successors)
}
34 changes: 34 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dsl/BlockStmt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jacodb.ets.dsl

sealed interface BlockStmt

data object BlockNop : BlockStmt

data class BlockAssign(
val target: Local,
val expr: Expr,
) : BlockStmt

data class BlockReturn(
val expr: Expr,
) : BlockStmt

data class BlockIf(
val condition: Expr,
) : BlockStmt
67 changes: 67 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dsl/DSL.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jacodb.ets.dsl

import org.jacodb.ets.utils.view

private fun main() {
val prog = program {
assign(local("i"), param(0))

ifStmt(gt(local("i"), const(10.0))) {
ifStmt(eq(local("i"), const(42.0))) {
ret(local("i"))
`else` {
assign(local("i"), const(10.0))
}
}
nop()
}

label("loop")
ifStmt(gt(local("i"), const(0.0))) {
assign(local("i"), sub(local("i"), const(1.0)))
goto("loop")
`else` {
ret(local("i"))
}
}

ret(const(42.0)) // unreachable
}

val doView = false

println("PROGRAM:")
println("-----")
println(prog.toText())
println("-----")

println("=== PROGRAM:")
println(prog.toDot())
if (doView) view(prog.toDot(), name = "program")

val blockCfg = prog.toBlockCfg()
println("=== BLOCK CFG:")
println(blockCfg.toDot())
if (doView) view(blockCfg.toDot(), name = "block")

val linearCfg = blockCfg.linearize()
println("=== LINEARIZED CFG:")
println(linearCfg.toDot())
if (doView) view(linearCfg.toDot(), name = "linear")
}
60 changes: 60 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dsl/Expr.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jacodb.ets.dsl

sealed interface Expr

data class Local(val name: String) : Expr {
override fun toString() = name
}

data class Parameter(val index: Int) : Expr {
override fun toString() = "param($index)"
}

object ThisRef : Expr {
override fun toString() = "this"
}

data class Constant(val value: Double) : Expr {
override fun toString() = "const($value)"
}

enum class BinaryOperator {
AND, OR,
EQ, NEQ, LT, LTE, GT, GTE,
ADD, SUB, MUL, DIV
}

data class BinaryExpr(
val operator: BinaryOperator,
val left: Expr,
val right: Expr,
) : Expr {
override fun toString() = "${operator.name.lowercase()}($left, $right)"
}

enum class UnaryOperator {
NOT, NEG
}

data class UnaryExpr(
val operator: UnaryOperator,
val expr: Expr,
) : Expr {
override fun toString() = "${operator.name.lowercase()}($expr)"
}
Loading

0 comments on commit d03090c

Please sign in to comment.