Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -6,12 +6,18 @@ import org.jacodb.ets.model.EtsIfStmt
import org.jacodb.ets.model.EtsReturnStmt
import org.jacodb.ets.model.EtsThrowStmt
import org.usvm.UBoolExpr
import org.usvm.machine.call.TsUnknownCallEvent
import org.usvm.machine.expr.TsSimpleValueResolver
import org.usvm.machine.interpreter.TsStepScope
import org.usvm.statistics.UInterpreterObserver

@Suppress("unused")
interface TsInterpreterObserver : UInterpreterObserver {
/** Called after the profile dispatcher selects an outcome for an unknown call. */
fun onUnknownCall(event: TsUnknownCallEvent) {
// default empty implementation
}

fun onAssignStatement(
simpleValueResolver: TsSimpleValueResolver,
stmt: EtsAssignStmt,
Expand Down
13 changes: 10 additions & 3 deletions usvm-ts/src/main/kotlin/org/usvm/machine/TsMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,17 @@ class TsMachine(
private val components = TsComponents(typeSystem, options)
private val ctx = TsContext(scene, components)
private val resolvedUnknownCallDispatcher = unknownCallDispatcher ?: TsProfileUnknownCallDispatcher(
tsOptions.unknownCallProfile,
unknownCallModelProvider,
profile = tsOptions.unknownCallProfile,
modelProvider = unknownCallModelProvider,
observer = observer,
)
private val interpreter = TsInterpreter(
ctx = ctx,
graph = graph,
options = tsOptions,
observer = observer,
unknownCallDispatcher = resolvedUnknownCallDispatcher,
)
private val interpreter = TsInterpreter(ctx, graph, tsOptions, observer, resolvedUnknownCallDispatcher)
private val cfgStatistics = CfgStatisticsImpl(graph)

fun analyze(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.usvm.machine.call

import mu.KotlinLogging
import org.jacodb.ets.model.EtsMethodSignature
import org.jacodb.ets.model.EtsStmt
import org.usvm.machine.TsInterpreterObserver

private val logger = KotlinLogging.logger {}

/** Explains why a call reached the residual fallback instead of a semantic model. */
enum class TsUnknownCallResidualReason {
MODEL_LOOKUP_DISABLED,
MODEL_NOT_APPLICABLE,
}

/** Describes the model or fallback action selected for one unknown call. */
sealed interface TsUnknownCallDecision {
data class ModelApplied(
val modelId: String,
) : TsUnknownCallDecision {
init {
require(modelId.isNotBlank()) { "Applied model ID must not be blank" }
}
}

data class ResidualFallback(
val policy: TsResidualCallPolicy,
val reason: TsUnknownCallResidualReason,
) : TsUnknownCallDecision
}

/** A structured decision reported for one unknown call. */
data class TsUnknownCallEvent(
val callSite: EtsStmt,
val callee: EtsMethodSignature,
val failureReason: TsUnknownCallFailureReason,
val profile: TsUnknownCallProfile,
val outcome: TsUnknownCallOutcome,
val decision: TsUnknownCallDecision,
)

internal fun TsInterpreterObserver.onUnknownCallSafely(event: TsUnknownCallEvent) {
runCatching { onUnknownCall(event) }
.onFailure { error -> logger.warn(error) { "Unknown-call observer failed while recording an event" } }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.usvm.machine.call

import org.jacodb.ets.model.EtsClassSignature
import org.usvm.api.mockMethodCall
import org.usvm.machine.TsInterpreterObserver
import org.usvm.machine.interpreter.TsStepScope
import org.usvm.machine.state.newStmt

Expand Down Expand Up @@ -61,15 +62,24 @@ object TsUnknownCallProfiles {
}

/** The result of asking a model provider to handle one unknown call. */
enum class TsUnknownCallModelApplication {
APPLIED,
NOT_APPLICABLE,
sealed interface TsUnknownCallModelApplication {
/** Identifies the semantic model that produced the successor states. */
data class Applied(
val modelId: String,
) : TsUnknownCallModelApplication {
init {
require(modelId.isNotBlank()) { "Applied model ID must not be blank" }
}
}

/** Indicates that the provider has no semantic model for this call. */
data object NotApplicable : TsUnknownCallModelApplication
}

/**
* Applies semantic models without exposing their lookup or registry implementation to the dispatcher.
*
* A provider returning [TsUnknownCallModelApplication.APPLIED] must update the supplied scope with the model's
* A provider returning [TsUnknownCallModelApplication.Applied] must update the supplied scope with the model's
* successor states. The deterministic registry and concrete model implementations are introduced separately.
*/
fun interface TsUnknownCallModelProvider {
Expand All @@ -79,33 +89,79 @@ fun interface TsUnknownCallModelProvider {
/** Empty provider used until an explicit model registry is configured. */
object TsNoUnknownCallModels : TsUnknownCallModelProvider {
override fun apply(scope: TsStepScope, call: TsUnknownCall): TsUnknownCallModelApplication =
TsUnknownCallModelApplication.NOT_APPLICABLE
TsUnknownCallModelApplication.NotApplicable
}

/** Applies the selected model/fallback profile to every residual call. */
class TsProfileUnknownCallDispatcher(
private val profile: TsUnknownCallProfile,
private val modelProvider: TsUnknownCallModelProvider,
private val observer: TsInterpreterObserver? = null,
) : TsUnknownCallDispatcher {
override fun dispatch(scope: TsStepScope, call: TsUnknownCall): TsUnknownCallOutcome {
if (profile.modelLookup == TsUnknownCallModelLookup.ENABLED &&
modelProvider.apply(scope, call) == TsUnknownCallModelApplication.APPLIED
) {
return TsUnknownCallOutcome.MODEL_APPLIED
val residualReason = when (profile.modelLookup) {
TsUnknownCallModelLookup.DISABLED -> {
TsUnknownCallResidualReason.MODEL_LOOKUP_DISABLED
}

TsUnknownCallModelLookup.ENABLED -> {
when (val application = modelProvider.apply(scope, call)) {
is TsUnknownCallModelApplication.Applied -> {
val event = event(
call = call,
outcome = TsUnknownCallOutcome.MODEL_APPLIED,
decision = TsUnknownCallDecision.ModelApplied(modelId = application.modelId),
)
observer?.onUnknownCallSafely(event)
return TsUnknownCallOutcome.MODEL_APPLIED
}

TsUnknownCallModelApplication.NotApplicable -> {
TsUnknownCallResidualReason.MODEL_NOT_APPLICABLE
}
}
}
}

return when (profile.residualPolicyFor(call)) {
val residualPolicy = profile.residualPolicyFor(call)
val outcome = when (residualPolicy) {
TsResidualCallPolicy.STOP_PATH -> TsUnknownCallOutcome.PATH_STOPPED
TsResidualCallPolicy.FRESH_SYMBOLIC_RETURN -> TsUnknownCallOutcome.FRESH_SYMBOLIC_RETURN
}
val event = event(
call = call,
outcome = outcome,
decision = TsUnknownCallDecision.ResidualFallback(
policy = residualPolicy,
reason = residualReason,
),
)
when (residualPolicy) {
TsResidualCallPolicy.STOP_PATH -> {
val falseExpr = scope.calcOnState { ctx.falseExpr }
scope.assert(falseExpr)
TsUnknownCallOutcome.PATH_STOPPED
}

TsResidualCallPolicy.FRESH_SYMBOLIC_RETURN -> {
mockMethodCall(scope, call.callee, call.resultType)
scope.doWithState { newStmt(call.callSite) }
TsUnknownCallOutcome.FRESH_SYMBOLIC_RETURN
}
}

observer?.onUnknownCallSafely(event)
return outcome
}

private fun event(
call: TsUnknownCall,
outcome: TsUnknownCallOutcome,
decision: TsUnknownCallDecision,
) = TsUnknownCallEvent(
callSite = call.callSite,
callee = call.callee,
failureReason = call.failureReason,
profile = profile.copy(residualOverrides = profile.residualOverrides.toMap()),
outcome = outcome,
decision = decision,
)
}
Loading
Loading