Skip to content

Commit 44d373f

Browse files
DamtevsergeypospelovCaelmBleidd
authored
Supported switch instruction in JaCoDB interpreter (#39)
--------- Co-authored-by: Sergey Pospelov <[email protected]> Co-authored-by: Alexey Menshutin <[email protected]>
1 parent 93628bd commit 44d373f

File tree

6 files changed

+302
-88
lines changed

6 files changed

+302
-88
lines changed

usvm-core/src/main/kotlin/org/usvm/State.kt

Lines changed: 105 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ abstract class UState<Type, Field, Method, Statement>(
1818
open val pathConstraints: UPathConstraints<Type>,
1919
open val memory: UMemoryBase<Field, Type, Method>,
2020
open var models: List<UModelBase<Field, Type>>,
21-
open var path: PersistentList<Statement>
21+
open var path: PersistentList<Statement>,
2222
) {
2323
/**
2424
* Deterministic state id.
@@ -40,67 +40,71 @@ abstract class UState<Type, Field, Method, Statement>(
4040
get() = path.lastOrNull()
4141
}
4242

43-
class ForkResult<T>(
43+
data class ForkResult<T>(
4444
val positiveState: T?,
4545
val negativeState: T?,
46-
) {
47-
operator fun component1(): T? = positiveState
48-
operator fun component2(): T? = negativeState
49-
}
46+
)
47+
48+
private typealias StateToCheck = Boolean
49+
50+
private const val ForkedState = true
51+
private const val OriginalState = false
5052

5153
/**
52-
* Checks if [conditionToCheck] is satisfiable within path constraints of [state].
53-
* If it does, clones [state] and returns it with enriched constraints:
54-
* - if [forkToSatisfied], then adds constraint [satisfiedCondition];
55-
* - if ![forkToSatisfied], then adds constraint [conditionToCheck].
56-
* Otherwise, returns null.
57-
* If [conditionToCheck] is not unsatisfiable (i.e., solver returns sat or unknown),
58-
* mutates [state] by adding new path constraint c:
59-
* - if [forkToSatisfied], then c = [conditionToCheck]
60-
* - if ![forkToSatisfied], then c = [satisfiedCondition]
54+
* Checks [newConstraintToOriginalState] or [newConstraintToForkedState], depending on the value of [stateToCheck].
55+
* Depending on the result of checking this condition, do the following:
56+
* - On [UUnsatResult] - returns `null`;
57+
* - On [UUnknownResult] - adds [newConstraintToOriginalState] to the path constraints of the [state],
58+
* iff [addConstraintOnUnknown] is `true`, and returns null;
59+
* - On [USatResult] - clones the original state and adds the [newConstraintToForkedState] to it, adds [newConstraintToOriginalState]
60+
* to the original state, sets the satisfiable model to the corresponding state depending on the [stateToCheck], and returns the
61+
* forked state.
62+
*
6163
*/
6264
private fun <T : UState<Type, Field, *, *>, Type, Field> forkIfSat(
6365
state: T,
64-
satisfiedCondition: UBoolExpr,
65-
conditionToCheck: UBoolExpr,
66-
forkToSatisfied: Boolean,
66+
newConstraintToOriginalState: UBoolExpr,
67+
newConstraintToForkedState: UBoolExpr,
68+
stateToCheck: StateToCheck,
69+
addConstraintOnUnknown: Boolean = true,
6770
): T? {
68-
val pathConstraints = state.pathConstraints.clone()
69-
pathConstraints += conditionToCheck
71+
val constraintsToCheck = state.pathConstraints.clone()
7072

71-
if (pathConstraints.isFalse) {
72-
return null
73+
constraintsToCheck += if (stateToCheck) {
74+
newConstraintToForkedState
75+
} else {
76+
newConstraintToOriginalState
7377
}
74-
75-
val solver = satisfiedCondition.uctx.solver<Field, Type, Any?>()
76-
val satResult = solver.check(pathConstraints, useSoftConstraints = true)
78+
val solver = newConstraintToForkedState.uctx.solver<Field, Type, Any?>()
79+
val satResult = solver.check(constraintsToCheck, useSoftConstraints = true)
7780

7881
return when (satResult) {
82+
is UUnsatResult -> null
83+
7984
is USatResult -> {
80-
if (forkToSatisfied) {
85+
// Note that we cannot extract common code here due to
86+
// heavy plusAssign operator in path constraints.
87+
// Therefore, it is better to reuse already constructed [constraintToCheck].
88+
if (stateToCheck) {
8189
@Suppress("UNCHECKED_CAST")
82-
val forkedState = state.clone() as T
83-
// TODO: implement path condition setter (don't forget to reset UMemoryBase:types!)
84-
state.pathConstraints += conditionToCheck
85-
state.models = listOf(satResult.model)
86-
forkedState.pathConstraints += satisfiedCondition
90+
val forkedState = state.clone(constraintsToCheck) as T
91+
state.pathConstraints += newConstraintToOriginalState
92+
forkedState.models = listOf(satResult.model)
8793
forkedState
8894
} else {
8995
@Suppress("UNCHECKED_CAST")
90-
val forkedState = state.clone(pathConstraints) as T
91-
state.pathConstraints += satisfiedCondition
92-
forkedState.models = listOf(satResult.model)
96+
val forkedState = state.clone() as T
97+
state.pathConstraints += newConstraintToOriginalState
98+
state.models = listOf(satResult.model)
99+
// TODO: implement path condition setter (don't forget to reset UMemoryBase:types!)
100+
forkedState.pathConstraints += newConstraintToForkedState
93101
forkedState
94102
}
95103
}
96104

97-
is UUnsatResult -> null
98-
99105
is UUnknownResult -> {
100-
if (forkToSatisfied) {
101-
state.pathConstraints += conditionToCheck
102-
} else {
103-
state.pathConstraints += satisfiedCondition
106+
if (addConstraintOnUnknown) {
107+
state.pathConstraints += newConstraintToOriginalState
104108
}
105109
null
106110
}
@@ -148,13 +152,18 @@ fun <T : UState<Type, Field, *, *>, Type, Field> fork(
148152

149153
trueModels.isNotEmpty() -> state to forkIfSat(
150154
state,
151-
condition,
152-
notCondition,
153-
forkToSatisfied = false
155+
newConstraintToOriginalState = condition,
156+
newConstraintToForkedState = notCondition,
157+
stateToCheck = ForkedState
154158
)
155159

156160
falseModels.isNotEmpty() -> {
157-
val forkedState = forkIfSat(state, notCondition, condition, forkToSatisfied = true)
161+
val forkedState = forkIfSat(
162+
state,
163+
newConstraintToOriginalState = condition,
164+
newConstraintToForkedState = notCondition,
165+
stateToCheck = OriginalState
166+
)
158167

159168
if (forkedState != null) {
160169
state to forkedState
@@ -169,3 +178,55 @@ fun <T : UState<Type, Field, *, *>, Type, Field> fork(
169178
@Suppress("UNCHECKED_CAST")
170179
return ForkResult(posState, negState as T?)
171180
}
181+
182+
/**
183+
* Implements symbolic branching on few disjoint conditions.
184+
*
185+
* @return a list of states for each condition - `null` state
186+
* means [UUnknownResult] or [UUnsatResult] of checking condition.
187+
*/
188+
fun <T : UState<Type, Field, *, *>, Type, Field> forkMulti(
189+
state: T,
190+
conditions: Iterable<UBoolExpr>,
191+
): List<T?> {
192+
var curState = state
193+
val result = mutableListOf<T?>()
194+
for (condition in conditions) {
195+
val (trueModels, _) = curState.models.partition { model ->
196+
val holdsInModel = model.eval(condition)
197+
check(holdsInModel is KInterpretedValue<UBoolSort>) {
198+
"Evaluation in $model on condition $condition: expected true or false, but got $holdsInModel"
199+
}
200+
holdsInModel.isTrue
201+
}
202+
203+
val nextRoot = if (trueModels.isNotEmpty()) {
204+
@Suppress("UNCHECKED_CAST")
205+
val root = curState.clone() as T
206+
207+
curState.models = trueModels
208+
curState.pathConstraints += condition
209+
210+
root
211+
} else {
212+
val root = forkIfSat(
213+
curState,
214+
newConstraintToOriginalState = condition,
215+
newConstraintToForkedState = condition.ctx.trueExpr,
216+
stateToCheck = OriginalState,
217+
addConstraintOnUnknown = false
218+
)
219+
220+
root
221+
}
222+
223+
if (nextRoot != null) {
224+
result += curState
225+
curState = nextRoot
226+
} else {
227+
result += null
228+
}
229+
}
230+
231+
return result
232+
}

usvm-core/src/main/kotlin/org/usvm/StepScope.kt

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
package org.usvm
22

3+
import org.usvm.StepScope.StepScopeState.CANNOT_BE_PROCESSED
4+
import org.usvm.StepScope.StepScopeState.CAN_BE_PROCESSED
5+
import org.usvm.StepScope.StepScopeState.DEAD
6+
37
/**
48
* An auxiliary class, which carefully maintains forks and asserts via [fork] and [assert].
59
* It should be created on every step in an interpreter.
610
* You can think about an instance of [StepScope] as a monad `ExceptT null (State [T])`.
711
*
8-
* An underlying state is `null`, iff one of the `condition`s passed to the [fork] was unsatisfiable.
12+
* This scope is considered as [DEAD], iff the condition in [assert] was unsatisfiable or unknown.
13+
* The underlying state cannot be processed further (see [CANNOT_BE_PROCESSED]),
14+
* if the first passed to [fork] or [forkMulti] condition was unsatisfiable or unknown.
915
*
1016
* To execute some function on a state, you should use [doWithState] or [calcOnState]. `null` is returned, when
11-
* the current state is `null`.
17+
* this scope cannot be processed on the current step - see [CANNOT_BE_PROCESSED].
1218
*
1319
* @param originalState an initial state.
1420
*/
1521
class StepScope<T : UState<Type, Field, *, *>, Type, Field>(
16-
originalState: T,
22+
private val originalState: T,
1723
) {
1824
private val forkedStates = mutableListOf<T>()
19-
private var curState: T? = originalState
20-
var alive: Boolean = true
21-
private set
25+
26+
private val alive: Boolean get() = stepScopeState != DEAD
27+
private val canProcessFurtherOnCurrentStep: Boolean get() = stepScopeState == CAN_BE_PROCESSED
28+
29+
/**
30+
* Determines whether we interact this scope on the current step.
31+
* @see [StepScopeState].
32+
*/
33+
private var stepScopeState: StepScopeState = CAN_BE_PROCESSED
2234

2335
/**
2436
* @return forked states and the status of initial state.
@@ -30,45 +42,54 @@ class StepScope<T : UState<Type, Field, *, *>, Type, Field>(
3042
*
3143
* @return `null` if the underlying state is `null`.
3244
*/
33-
fun doWithState(block: T.() -> Unit): Unit? {
34-
val state = curState ?: return null
35-
state.block()
36-
return Unit
37-
}
45+
fun doWithState(block: T.() -> Unit): Unit? =
46+
if (canProcessFurtherOnCurrentStep) {
47+
originalState.block()
48+
} else {
49+
null
50+
}
3851

3952
/**
4053
* Executes [block] on a state.
4154
*
4255
* @return `null` if the underlying state is `null`, otherwise returns result of calling [block].
4356
*/
44-
fun <R> calcOnState(block: T.() -> R): R? {
45-
val state = curState ?: return null
46-
return state.block()
47-
}
57+
fun <R> calcOnState(block: T.() -> R): R? =
58+
if (canProcessFurtherOnCurrentStep) {
59+
originalState.block()
60+
} else {
61+
null
62+
}
4863

4964
/**
5065
* Forks on a [condition], performing [blockOnTrueState] on a state satisfying [condition] and
5166
* [blockOnFalseState] on a state satisfying [condition].not().
5267
*
53-
* If the [condition] is unsatisfiable, sets underlying state to `null`.
68+
* If the [condition] is unsatisfiable or unknown, sets the scope state to the [CANNOT_BE_PROCESSED].
5469
*
55-
* @return `null` if the [condition] is unsatisfiable.
70+
* @return `null` if the [condition] is unsatisfiable or unknown.
5671
*/
5772
fun fork(
5873
condition: UBoolExpr,
5974
blockOnTrueState: T.() -> Unit = {},
6075
blockOnFalseState: T.() -> Unit = {},
6176
): Unit? {
62-
val state = curState ?: return null
77+
check(canProcessFurtherOnCurrentStep)
6378

64-
val (posState, negState) = fork(state, condition)
79+
val (posState, negState) = fork(originalState, condition)
6580

6681
posState?.blockOnTrueState()
67-
curState = posState
82+
83+
if (posState == null) {
84+
stepScopeState = CANNOT_BE_PROCESSED
85+
check(negState === originalState)
86+
} else {
87+
check(posState === originalState)
88+
}
6889

6990
if (negState != null) {
7091
negState.blockOnFalseState()
71-
if (negState !== state) {
92+
if (negState !== originalState) {
7293
forkedStates += negState
7394
}
7495
}
@@ -77,23 +98,75 @@ class StepScope<T : UState<Type, Field, *, *>, Type, Field>(
7798
return posState?.let { }
7899
}
79100

101+
/**
102+
* Forks on a few disjoint conditions using `forkMulti` in `State.kt`
103+
* and executes the corresponding block on each not-null state.
104+
*
105+
* NOTE: always sets the [stepScopeState] to the [CANNOT_BE_PROCESSED] value.
106+
*/
107+
fun forkMulti(conditionsWithBlockOnStates: List<Pair<UBoolExpr, T.() -> Unit>>) {
108+
check(canProcessFurtherOnCurrentStep)
109+
110+
val conditions = conditionsWithBlockOnStates.map { it.first }
111+
112+
val conditionStates = forkMulti(originalState, conditions)
113+
114+
val forkedStates = conditionStates.mapIndexedNotNull { idx, positiveState ->
115+
val block = conditionsWithBlockOnStates[idx].second
116+
117+
positiveState?.apply(block)
118+
}
119+
120+
stepScopeState = CANNOT_BE_PROCESSED
121+
if (forkedStates.isEmpty()) {
122+
stepScopeState = DEAD
123+
return
124+
}
125+
126+
val firstForkedState = forkedStates.first()
127+
require(firstForkedState == originalState) {
128+
"The original state $originalState was expected to become the first of forked states but $firstForkedState found"
129+
}
130+
131+
// Interpret the first state as original and others as forked
132+
this.forkedStates += forkedStates.subList(1, forkedStates.size)
133+
}
134+
80135
fun assert(
81136
constraint: UBoolExpr,
82137
block: T.() -> Unit = {},
83138
): Unit? {
84-
val state = curState ?: return null
139+
check(canProcessFurtherOnCurrentStep)
85140

86-
val (posState, _) = fork(state, constraint)
141+
val (posState, _) = fork(originalState, constraint)
87142

88143
posState?.block()
89-
curState = posState
90144

91145
if (posState == null) {
92-
alive = false
146+
stepScopeState = DEAD
93147
}
94148

95149
return posState?.let { }
96150
}
151+
152+
/**
153+
* Represents the current state of this [StepScope].
154+
*/
155+
private enum class StepScopeState {
156+
/**
157+
* Cannot be processed further with any actions.
158+
*/
159+
DEAD,
160+
/**
161+
* Cannot be forked or asserted using [fork], [forkMulti] or [assert],
162+
* but is considered as alive from the Machine's point of view.
163+
*/
164+
CANNOT_BE_PROCESSED,
165+
/**
166+
* Can be forked using [fork] or [forkMulti] and asserted using [assert].
167+
*/
168+
CAN_BE_PROCESSED;
169+
}
97170
}
98171

99172
/**

0 commit comments

Comments
 (0)