Skip to content

Commit

Permalink
Add exceptions support (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
CaelmBleidd authored Jul 24, 2023
1 parent f7dbc43 commit 84d632d
Show file tree
Hide file tree
Showing 57 changed files with 671 additions and 268 deletions.
25 changes: 25 additions & 0 deletions usvm-core/src/main/kotlin/org/usvm/CallStack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ data class UCallStackFrame<Method, Statement>(
val returnSite: Statement?,
)

data class UStackTraceFrame<Method, Statement>(
val method: Method,
val instruction: Statement,
)

class UCallStack<Method, Statement> private constructor(
private val stack: ArrayDeque<UCallStackFrame<Method, Statement>>,
) : Collection<UCallStackFrame<Method, Statement>> by stack {
Expand All @@ -29,4 +34,24 @@ class UCallStack<Method, Statement> private constructor(
newStack.addAll(stack)
return UCallStack(newStack)
}

fun stackTrace(currentInstruction: Statement): List<UStackTraceFrame<Method, Statement>> {
val stacktrace = stack
.asSequence()
.zipWithNext { first, second -> UStackTraceFrame<Method, Statement>(first.method, second.returnSite!!) }
.toMutableList()

stacktrace += UStackTraceFrame(stack.last().method, currentInstruction)

return stacktrace
}

override fun toString(): String {
var frameCounter = 0

return joinToString(
prefix = "Call stack (contains $size frame${if (size > 1) "s" else ""}):${System.lineSeparator()}",
separator = System.lineSeparator()
) { "\t${frameCounter++}: $it" }
}
}
4 changes: 4 additions & 0 deletions usvm-core/src/main/kotlin/org/usvm/Machine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ abstract class UMachine<State> : AutoCloseable {
pathSelector.add(aliveForkedStates)
}
}

if (!pathSelector.isEmpty()) {
logger.debug { stopStrategy.stopReason() }
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions usvm-core/src/main/kotlin/org/usvm/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ abstract class UState<Type, Field, Method, Statement>(
// TODO or last? Do we add a current stmt into the path immediately?
val currentStatement: Statement?
get() = path.lastOrNull()

/**
* A property containing information about whether the state is exceptional or not.
*/
abstract val isExceptional: Boolean
}

data class ForkResult<T>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.usvm.ps

import org.usvm.UPathSelector
import org.usvm.UState
import java.util.IdentityHashMap

/**
* A class designed to give the highest priority to the states containing exceptions.
*/
class ExceptionPropagationPathSelector<State : UState<*, *, *, *>>(
private val selector: UPathSelector<State>,
) : UPathSelector<State> {
// An internal queue for states containing exceptions.
// Note that we use an identity map here to be able
// to determine whether some state is already in the queue or not
// without extra time consumption.
// We use only keys from this map.
private val exceptionalStates = IdentityHashMap<State, Nothing>()

// An identity map of the states that were added in the internal selector.
// That means that these states present not only in the internal queue, but in
// the queue of the selector, and we have to process them as well.
// We use only keys from this map.
private val statesInSelector = IdentityHashMap<State, Nothing>()

override fun isEmpty(): Boolean = exceptionalStates.isEmpty() && selector.isEmpty()

/**
* Returns an exceptional state from the internal queue if it is not empty, or
* the result of [peek] method called on the wrapper selector.
*/
override fun peek(): State = exceptionalStates.keys.lastOrNull() ?: selector.peek()

override fun update(state: State) {
val alreadyInExceptionalStates = state in exceptionalStates
val isExceptional = state.isExceptional

when {
// A case when the state became exceptional, but at the
// previous step it didn't contain any exceptions.
// We add this exceptional state to the internal queue.
isExceptional && !alreadyInExceptionalStates -> {
exceptionalStates += state to null
}
// A case when the state contained an exception at the previous step,
// but it doesn't contain it anymore. There are two possibilities:
// this state was either added directly in the internal queue,
// e.g., after a fork operation, or was transformed from a regular one.
// In the first case, we have to add it to the selector and remove from the
// exceptional queue, in the second one just removal is enough since
// it was added in the selector earlier.
!isExceptional && alreadyInExceptionalStates -> {
exceptionalStates -= state

if (state !in statesInSelector) {
selector.add(listOf(state))
statesInSelector += state to null
}
}

// Other operations don't cause queues transformations.
else -> Unit // do nothing
}

// If the state is contained in the selector, we must call `update` operation for it.
if (state in statesInSelector) {
selector.update(state)
}
}

/**
* Add the [states] in the selector. Depending on whether it
* is exceptional or not, it will be added in the exceptional
* state queue or in the wrapper selector.
*/
override fun add(states: Collection<State>) {
val statesToAdd = mutableListOf<State>()

states.forEach {
// It if it is an exceptional state, we don't have to worry whether it
// is contains in the selector or not. It was either already added in it,
// or will be added later if required, when it becomes unexceptional one.
if (it.isExceptional) {
exceptionalStates += it to null
} else {
// Otherwise, we simply add it to the selector directly.
statesToAdd += it
statesInSelector += it to null
}
}

selector.add(statesToAdd)
}

override fun remove(state: State) {
if (state in exceptionalStates) {
exceptionalStates -= state
}

if (state in statesInSelector) {
selector.remove(state)
}
}
}
98 changes: 69 additions & 29 deletions usvm-core/src/main/kotlin/org/usvm/ps/PathSelectorFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import org.usvm.UState
import org.usvm.statistics.CoverageStatistics
import org.usvm.statistics.DistanceStatistics
import org.usvm.statistics.PathsTreeStatistics
import org.usvm.util.RandomizedPriorityCollection
import org.usvm.util.DeterministicPriorityCollection
import org.usvm.util.RandomizedPriorityCollection
import kotlin.math.max
import kotlin.random.Random

Expand All @@ -18,8 +18,8 @@ fun <Method, Statement, State : UState<*, *, Method, Statement>> createPathSelec
options: UMachineOptions,
pathsTreeStatistics: () -> PathsTreeStatistics<Method, Statement, State>? = { null },
coverageStatistics: () -> CoverageStatistics<Method, Statement, State>? = { null },
distanceStatistics: () -> DistanceStatistics<Method, Statement>? = { null }
) : UPathSelector<State> {
distanceStatistics: () -> DistanceStatistics<Method, Statement>? = { null },
): UPathSelector<State> {
val strategies = options.pathSelectionStrategies
require(strategies.isNotEmpty()) { "At least one path selector strategy should be specified" }

Expand All @@ -30,22 +30,26 @@ fun <Method, Statement, State : UState<*, *, Method, Statement>> createPathSelec
PathSelectionStrategy.BFS -> BfsPathSelector()
PathSelectionStrategy.DFS -> DfsPathSelector()
PathSelectionStrategy.RANDOM_PATH -> RandomTreePathSelector(
requireNotNull(pathsTreeStatistics()) { "Paths tree statistics is required for random tree path selector" },
{ random.nextInt(0, Int.MAX_VALUE) }
pathsTreeStatistics = requireNotNull(pathsTreeStatistics()) { "Paths tree statistics is required for random tree path selector" },
randomNonNegativeInt = { random.nextInt(0, Int.MAX_VALUE) }
)

PathSelectionStrategy.DEPTH -> createDepthPathSelector()
PathSelectionStrategy.DEPTH_RANDOM -> createDepthPathSelector(random)
PathSelectionStrategy.FORK_DEPTH -> createForkDepthPathSelector(
requireNotNull(pathsTreeStatistics()) { "Paths tree statistics is required for fork depth path selector" }
)

PathSelectionStrategy.FORK_DEPTH_RANDOM -> createForkDepthPathSelector(
requireNotNull(pathsTreeStatistics()) { "Paths tree statistics is required for fork depth path selector" },
random
)

PathSelectionStrategy.CLOSEST_TO_UNCOVERED -> createClosestToUncoveredPathSelector(
requireNotNull(coverageStatistics()) { "Coverage statistics is required for closest to uncovered path selector" },
requireNotNull(distanceStatistics()) { "Distance statistics is required for closest to uncovered path selector" }
)

PathSelectionStrategy.CLOSEST_TO_UNCOVERED_RANDOM -> createClosestToUncoveredPathSelector(
requireNotNull(coverageStatistics()) { "Coverage statistics is required for closest to uncovered path selector" },
requireNotNull(distanceStatistics()) { "Distance statistics is required for closest to uncovered path selector" },
Expand All @@ -54,71 +58,107 @@ fun <Method, Statement, State : UState<*, *, Method, Statement>> createPathSelec
}
}

if (selectors.size == 1) {
val selector = selectors.single()
selector.add(listOf(initialState))
return selector
val propagateExceptions = options.exceptionsPropagation

selectors.singleOrNull()?.let { selector ->
val resultSelector = selector.wrapIfRequired(propagateExceptions)
resultSelector.add(listOf(initialState))
return resultSelector
}

return when (options.pathSelectorCombinationStrategy) {
require(selectors.size >= 2) { "Cannot create collaborative path selector from less than 2 selectors" }

val selector = when (options.pathSelectorCombinationStrategy) {
PathSelectorCombinationStrategy.INTERLEAVED -> {
val interleavedPathSelector = InterleavedPathSelector(selectors)
// Since all selectors here work as one, we can wrap an interleaved selector only.
val interleavedPathSelector = InterleavedPathSelector(selectors).wrapIfRequired(propagateExceptions)
interleavedPathSelector.add(listOf(initialState))
interleavedPathSelector
}

PathSelectorCombinationStrategy.PARALLEL -> {
selectors.first().add(listOf(initialState))
selectors.drop(1).forEach {
// Here we should wrap all selectors independently since they work in parallel.
val wrappedSelectors = selectors.map { it.wrapIfRequired(propagateExceptions) }

wrappedSelectors.first().add(listOf(initialState))
wrappedSelectors.drop(1).forEach {
@Suppress("UNCHECKED_CAST")
it.add(listOf(initialState.clone() as State))
}
ParallelPathSelector(selectors)

ParallelPathSelector(wrappedSelectors)
}
}

return selector
}

/**
* Wraps the selector into an [ExceptionPropagationPathSelector] if [propagateExceptions] is true.
*/
private fun <State : UState<*, *, *, *>> UPathSelector<State>.wrapIfRequired(propagateExceptions: Boolean) =
if (propagateExceptions && this !is ExceptionPropagationPathSelector<State>) {
ExceptionPropagationPathSelector(this)
} else {
this
}

private fun <State : UState<*, *, *, *>> compareById(): Comparator<State> = compareBy { it.id }

private fun <State : UState<*, *, *, *>> createDepthPathSelector(random: Random? = null): UPathSelector<State> {
if (random == null) {
return WeightedPathSelector({ DeterministicPriorityCollection(Comparator.naturalOrder()) }) { it.path.size }
return WeightedPathSelector(
priorityCollectionFactory = { DeterministicPriorityCollection(Comparator.naturalOrder()) },
weighter = { it.path.size }
)
}

// Notice: random never returns 1.0
return WeightedPathSelector({ RandomizedPriorityCollection(compareById()) { random.nextDouble() } }) { 1.0 / max(it.path.size, 1) }
return WeightedPathSelector(
priorityCollectionFactory = { RandomizedPriorityCollection(compareById()) { random.nextDouble() } },
weighter = { 1.0 / max(it.path.size, 1) }
)
}

private fun <Method, Statement, State : UState<*, *, Method, Statement>> createClosestToUncoveredPathSelector(
coverageStatistics: CoverageStatistics<Method, Statement, State>,
distanceStatistics: DistanceStatistics<Method, Statement>,
random: Random? = null
random: Random? = null,
): UPathSelector<State> {
val weighter = ShortestDistanceToTargetsStateWeighter(
coverageStatistics.getUncoveredStatements(),
distanceStatistics::getShortestCfgDistance,
distanceStatistics::getShortestCfgDistanceToExitPoint
targets = coverageStatistics.getUncoveredStatements(),
getCfgDistance = distanceStatistics::getShortestCfgDistance,
getCfgDistanceToExitPoint = distanceStatistics::getShortestCfgDistanceToExitPoint
)

coverageStatistics.addOnCoveredObserver { _, method, statement -> weighter.removeTarget(method, statement) }

if (random == null) {
return WeightedPathSelector({ DeterministicPriorityCollection(Comparator.naturalOrder()) }, weighter)
return WeightedPathSelector(
priorityCollectionFactory = { DeterministicPriorityCollection(Comparator.naturalOrder()) },
weighter = weighter
)
}

return WeightedPathSelector({ RandomizedPriorityCollection(compareById()) { random.nextDouble() } }) {
1.0 / max(weighter.weight(it).toDouble(), 1.0)
}
return WeightedPathSelector(
priorityCollectionFactory = { RandomizedPriorityCollection(compareById()) { random.nextDouble() } },
weighter = { 1.0 / max(weighter.weight(it).toDouble(), 1.0) }
)
}

private fun <Method, Statement, State : UState<*, *, Method, Statement>> createForkDepthPathSelector(
pathsTreeStatistics: PathsTreeStatistics<Method, Statement, State>,
random: Random? = null
random: Random? = null,
): UPathSelector<State> {
if (random == null) {
return WeightedPathSelector({ DeterministicPriorityCollection(Comparator.naturalOrder()) }) { pathsTreeStatistics.getStateDepth(it) }
return WeightedPathSelector(
priorityCollectionFactory = { DeterministicPriorityCollection(Comparator.naturalOrder()) },
weighter = { pathsTreeStatistics.getStateDepth(it) }
)
}

return WeightedPathSelector({ RandomizedPriorityCollection(compareById()) { random.nextDouble() } }) {
1.0 / max(pathsTreeStatistics.getStateDepth(it).toDouble(), 1.0)
}
return WeightedPathSelector(
priorityCollectionFactory = { RandomizedPriorityCollection(compareById()) { random.nextDouble() } },
weighter = { 1.0 / max(pathsTreeStatistics.getStateDepth(it).toDouble(), 1.0) }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,30 @@ class CoverageStatistics<Method, Statement, State : UState<*, *, Method, Stateme

init {
for (method in methods) {
val methodStatements = bfsTraversal(applicationGraph.entryPoints(method).toList(), applicationGraph::successors)
.fold(ConcurrentHashMap.newKeySet<Statement>() as MutableSet<Statement>) { acc, stmt -> acc.add(stmt); acc }
uncoveredStatements[method] = methodStatements
totalUncoveredStatements += methodStatements.size
coveredStatements[method] = HashSet()
addCoverageZone(method)
}
}

/**
* Adds a new zone of coverage retrieved from the method.
* This method might be useful to add coverage zones dynamically,
* e.g., like in the [TransitiveCoverageZoneObserver].
*/
fun addCoverageZone(method: Method) {
if (method in uncoveredStatements) return

val methodStatements = bfsTraversal(
applicationGraph.entryPoints(method).toList(),
applicationGraph::successors
).fold(ConcurrentHashMap.newKeySet<Statement>() as MutableSet<Statement>) { acc, stmt ->
acc.add(stmt); acc
}

uncoveredStatements[method] = methodStatements
totalUncoveredStatements += methodStatements.size
coveredStatements[method] = HashSet()
}

private fun computeCoverage(covered: Int, uncovered: Int): Float {
if (uncovered == 0) {
return 100f
Expand Down
Loading

0 comments on commit 84d632d

Please sign in to comment.