diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageInformation.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageInformation.kt index 0129b681faa..338bac123fb 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageInformation.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageInformation.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.backend.konan.llvm.column import org.jetbrains.kotlin.backend.konan.llvm.line import org.jetbrains.kotlin.backend.konan.llvm.symbolName import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.declarations.IrFile import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.name @@ -19,27 +20,45 @@ import org.jetbrains.kotlin.ir.declarations.name * Besides the obvious [file] and line/column borders, it has [RegionKind] which is described later. */ class Region( + startOffset: Int, + endOffset: Int, val file: IrFile, - val startLine: Int, - val startColumn: Int, - val endLine: Int, - val endColumn: Int, val kind: RegionKind ) { + var startOffset: Int = startOffset + set(value) { + if (value != UNDEFINED_OFFSET) { + field = value + } + } + + var endOffset: Int = endOffset + set(value) { + if (value != UNDEFINED_OFFSET) { + field = value + } + } + + val startLine: Int + get() = file.fileEntry.line(startOffset) + + val startColumn: Int + get() = file.fileEntry.column(startOffset) + + val endLine: Int + get() = file.fileEntry.line(endOffset) + + val endColumn: Int + get() = file.fileEntry.column(endOffset) + companion object { - fun fromIr(irElement: IrElement, irFile: IrFile, kind: RegionKind) = + fun fromIr(irElement: IrElement, irFile: IrFile, kind: RegionKind = RegionKind.Code) = fromOffset(irElement.startOffset, irElement.endOffset, irFile, kind) - fun fromOffset(startOffset: Int, endOffset: Int, irFile: IrFile, kind: RegionKind) = - Region( - irFile, - irFile.fileEntry.line(startOffset), - irFile.fileEntry.column(startOffset), - irFile.fileEntry.line(endOffset), - irFile.fileEntry.column(endOffset), - kind - ) + fun fromOffset(startOffset: Int, endOffset: Int, irFile: IrFile, kind: RegionKind = RegionKind.Code) = + if (startOffset == UNDEFINED_OFFSET || endOffset == UNDEFINED_OFFSET || startOffset == endOffset) null + else Region(startOffset, endOffset, irFile, kind) } override fun toString(): String { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageRegionCollector.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageRegionCollector.kt index 8d0b9e13007..f4fbc6c89ca 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageRegionCollector.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/coverage/CoverageRegionCollector.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.backend.konan.llvm.coverage import org.jetbrains.kotlin.backend.common.pop import org.jetbrains.kotlin.backend.common.push import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.expressions.* @@ -71,35 +72,63 @@ private class IrFunctionRegionsCollector( val irFile: IrFile ) : IrElementVisitorVoid { + private data class StatementContext(val current: IrStatement, val next: IrStatement?) + + val regions = mutableMapOf() + private val irFileStack = mutableListOf(irFile) + private val regionStack = mutableListOf() + + private val irStatementsStack = mutableListOf() + private val currentFile: IrFile get() = irFileStack.last() - val regions = mutableMapOf() + private val currentRegion: Region + get() = regionStack.last() override fun visitElement(element: IrElement) { element.acceptChildrenVoid(this) } override fun visitSimpleFunction(declaration: IrSimpleFunction) { - declaration.body?.let { - recordRegion(it) - it.acceptChildrenVoid(this) + declaration.body?.let { body -> + visitInRegionContext(recordRegion(body) ?: return) { + body.acceptVoid(this) + } } } override fun visitConstructor(declaration: IrConstructor) { val statements = declaration.body?.statements ?: return - statements.forEach { - if (it is IrDelegatingConstructorCall && !declaration.isPrimary - || it !is IrDelegatingConstructorCall && it !is IrReturn) { - recordRegion(it) - it.acceptVoid(this) + visitInStatementContext(statements) { statement -> + if (statement is IrDelegatingConstructorCall && !declaration.isPrimary + || statement !is IrDelegatingConstructorCall && statement !is IrReturn) { + recordRegion(statement) + statement.acceptVoid(this) } } } + override fun visitBody(body: IrBody) = visitInStatementContext(body.statements) + + override fun visitContainerExpression(expression: IrContainerExpression) { + val statements = expression.statements + when (expression) { + is IrReturnableBlock -> { + val file = expression.sourceFileSymbol?.owner + if (file != null && file != currentFile && fileFilter(file)) { + recordRegion(expression) + visitInFileContext(file) { + visitInStatementContext(statements) + } + } + } + else -> visitInStatementContext(statements) + } + } + // TODO: The following implementation produces correct region mapping, but something goes wrong later // override fun visitFieldAccess(expression: IrFieldAccessExpression) { // expression.receiver?.let { recordRegion(it) } @@ -117,48 +146,72 @@ private class IrFunctionRegionsCollector( } else { recordRegion(condition) recordRegion(result, condition.endOffset, result.endOffset) - condition.acceptChildrenVoid(this) + condition.acceptVoid(this) } - result.acceptChildrenVoid(this) + result.acceptVoid(this) } } override fun visitLoop(loop: IrLoop) { val condition = loop.condition recordRegion(condition) - condition.acceptChildrenVoid(this) + condition.acceptVoid(this) val body = loop.body ?: return when (loop) { is IrWhileLoop -> recordRegion(body, condition.endOffset, body.endOffset) is IrDoWhileLoop -> recordRegion(body, body.startOffset, condition.startOffset) } - body.acceptChildrenVoid(this) + body.acceptVoid(this) } - override fun visitBlock(expression: IrBlock) { - when (expression) { - is IrReturnableBlock -> { - val file = (expression.sourceFileSymbol?.owner) - if (file != null && file != currentFile && fileFilter(file)) { - recordRegion(expression) - irFileStack.push(file) - expression.acceptChildrenVoid(this) - irFileStack.pop() - } + override fun visitReturn(expression: IrReturn) { + val next = irStatementsStack.lastOrNull()?.next ?: return + val nextRegion = recordRegion(next, expression.endOffset, currentRegion.endOffset) ?: return + currentRegion.endOffset = expression.endOffset + regionStack.pop() + regionStack.push(nextRegion) + } + + override fun visitBreakContinue(jump: IrBreakContinue) { + val (current, next) = irStatementsStack.lastOrNull() ?: return + recordRegion(next ?: return, current.endOffset, jump.loop.endOffset) + } + + private fun visitInFileContext(file: IrFile, visit: () -> Unit) { + irFileStack.push(file) + visit() + irFileStack.pop() + } + + private fun visitInRegionContext(region: Region, visit: () -> Unit) { + regionStack.push(region) + visit() + regionStack.pop() + } + + private fun visitInStatementContext( + statements: List, + visit: (IrStatement) -> Unit = { statement -> statement.acceptVoid(this) } + ) { + for (i in 0..statements.lastIndex) { + val current = statements[i] + if (!current.hasValidOffsets()) { + continue } - else -> expression.acceptChildrenVoid(this) + val next = if (i < statements.lastIndex && statements[i + 1].hasValidOffsets()) statements[i + 1] else null + irStatementsStack.push(StatementContext(current, next)) + visit(current) + irStatementsStack.pop() } } - private fun recordRegion(irElement: IrElement, kind: RegionKind = RegionKind.Code) { - recordRegion(irElement, irElement.startOffset, irElement.endOffset, kind) - } + private fun recordRegion(irElement: IrElement, kind: RegionKind = RegionKind.Code) + = Region.fromIr(irElement, currentFile, kind)?.also { regions[irElement] = it } - private fun recordRegion(irElement: IrElement, startOffset: Int, endOffset: Int, kind: RegionKind = RegionKind.Code) { - if (startOffset == UNDEFINED_OFFSET || endOffset == UNDEFINED_OFFSET) { - return - } - regions[irElement] = Region.fromOffset(startOffset, endOffset, currentFile, kind) - } + private fun recordRegion(irElement: IrElement, startOffset: Int, endOffset: Int, kind: RegionKind = RegionKind.Code) + = Region.fromOffset(startOffset, endOffset, currentFile, kind)?.also { regions[irElement] = it } + + private fun IrElement.hasValidOffsets() = startOffset != UNDEFINED_OFFSET && endOffset != UNDEFINED_OFFSET + && startOffset != endOffset } \ No newline at end of file diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index 6868dac588e..413aee2882f 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -3961,6 +3961,21 @@ if (UtilsKt.testTargetSupportsCodeCoverage(project)) { } } + task coverage_jumps(type: CoverageTest) { + binaryName = "CoverageJumps" + numberOfCoveredFunctions = 7 + numberOfCoveredLines = 66 + + konanArtifacts { + program(binaryName, targets: [ target ]) { + srcDir 'coverage/basic/jumps' + baseDir "$testOutputCoverage/$binaryName" + entryPoint "coverage.basic.jumps.main" + extraOpts "-Xcoverage", "-Xcoverage-file=$profrawFile" + } + } + } + task coverage_smoke0(type: CoverageTest) { binaryName = "CoverageSmoke0" numberOfCoveredFunctions = 2 diff --git a/backend.native/tests/coverage/basic/jumps/main.kt b/backend.native/tests/coverage/basic/jumps/main.kt new file mode 100644 index 00000000000..249f3c75090 --- /dev/null +++ b/backend.native/tests/coverage/basic/jumps/main.kt @@ -0,0 +1,74 @@ +package coverage.basic.jumps + +fun simpleReturn(n: Int) { + if (n == 0) return + println(n) +} + +fun returnFromIfBranch(n: Int) { + if (n > 0) { + if (n > 10) { + return + } + } else if (n < -10) { + return + } else if (n == 0) { + return + } + println(n) +} + +fun returnFromWhenBranch(n: Int) { + when { + n == 0 -> return + n == 1 -> return + n == 2 -> { + println(n) + return + } + else -> println(n) + } +} + +fun breakFromWhile() { + var a = 7 + while (true) { + if (a < 4) break + println(a) + a-- + } +} + +fun continueFromDoWhile() { + var a = 0 + do { + if (a % 3 == 0) { + a++ + continue + } + println(a) + a++ + } while (a < 10) +} + +fun singleReturn() { + return +} + +fun main() { + simpleReturn(0) + simpleReturn(1) + + returnFromIfBranch(1) + returnFromIfBranch(11) + returnFromIfBranch(-11) + returnFromIfBranch(0) + + returnFromWhenBranch(0) + returnFromWhenBranch(1) + returnFromWhenBranch(2) + + breakFromWhile() + continueFromDoWhile() + singleReturn() +} \ No newline at end of file