Skip to content

Commit d777277

Browse files
A first attempt to implement element and array types connection
1 parent d0b4b15 commit d777277

File tree

17 files changed

+265
-92
lines changed

17 files changed

+265
-92
lines changed

usvm-core/src/main/kotlin/org/usvm/constraints/TypeConstraints.kt

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ interface UTypeEvaluator<Type> {
3030
* Manages allocated objects separately from input ones. Indeed, we know the type of an allocated object
3131
* precisely, thus we can evaluate the subtyping constraints for them concretely (modulo generic type variables).
3232
*/
33-
class UTypeConstraints<Type>(
33+
open class UTypeConstraints<Type>(
3434
private val typeSystem: UTypeSystem<Type>,
3535
private val equalityConstraints: UEqualityConstraints,
36-
private val concreteRefToType: MutableMap<UConcreteHeapAddress, Type> = mutableMapOf(),
36+
protected val concreteRefToType: MutableMap<UConcreteHeapAddress, Type> = mutableMapOf(),
3737
symbolicRefToTypeRegion: MutableMap<USymbolicHeapRef, UTypeRegion<Type>> = mutableMapOf(),
3838
) : UTypeEvaluator<Type> {
3939
init {
40-
equalityConstraints.subscribe(::intersectConstraints)
40+
equalityConstraints.subscribe(::intersectRegions)
4141
}
4242

4343
val symbolicRefToTypeRegion get(): Map<USymbolicHeapRef, UTypeRegion<Type>> = _symbolicRefToTypeRegion
@@ -57,7 +57,7 @@ class UTypeConstraints<Type>(
5757
)
5858
}
5959

60-
private fun contradiction() {
60+
protected fun contradiction() {
6161
isContradicting = true
6262
}
6363

@@ -68,11 +68,13 @@ class UTypeConstraints<Type>(
6868
concreteRefToType[ref] = type
6969
}
7070

71-
private fun getRegion(symbolicRef: USymbolicHeapRef) =
72-
_symbolicRefToTypeRegion[equalityConstraints.equalReferences.find(symbolicRef)] ?: topTypeRegion
71+
fun getTypeRegion(symbolicRef: USymbolicHeapRef, useRepresentative: Boolean = false): UTypeRegion<Type> {
72+
val representative = if (useRepresentative) equalityConstraints.equalReferences.find(symbolicRef) else symbolicRef
73+
return _symbolicRefToTypeRegion[representative] ?: topTypeRegion
74+
}
7375

7476

75-
private fun setRegion(symbolicRef: USymbolicHeapRef, value: UTypeRegion<Type>) {
77+
private fun setTypeRegion(symbolicRef: USymbolicHeapRef, value: UTypeRegion<Type>) {
7678
_symbolicRefToTypeRegion[equalityConstraints.equalReferences.find(symbolicRef)] = value
7779
}
7880

@@ -96,23 +98,7 @@ class UTypeConstraints<Type>(
9698
}
9799

98100
is USymbolicHeapRef -> {
99-
val constraints = getRegion(ref)
100-
val newConstraints = constraints.addSupertype(type)
101-
if (newConstraints.isContradicting) {
102-
// the only left option here is to be equal to null
103-
equalityConstraints.makeEqual(ref, ref.uctx.nullRef)
104-
} else {
105-
// Inferring new symbolic disequalities here
106-
for ((key, value) in _symbolicRefToTypeRegion.entries) {
107-
// TODO: cache intersections?
108-
if (key != ref && value.intersect(newConstraints).isEmpty) {
109-
// If we have two inputs of incomparable reference types, then they are either non equal,
110-
// or both nulls
111-
equalityConstraints.makeNonEqualOrBothNull(ref, key)
112-
}
113-
}
114-
setRegion(ref, newConstraints)
115-
}
101+
updateRegionCanBeEqualNull(ref) { it.addSupertype(type) }
116102
}
117103

118104
else -> error("Provided heap ref must be either concrete or purely symbolic one, found $ref")
@@ -139,23 +125,7 @@ class UTypeConstraints<Type>(
139125
}
140126

141127
is USymbolicHeapRef -> {
142-
val constraints = getRegion(ref)
143-
val newConstraints = constraints.excludeSupertype(type)
144-
equalityConstraints.makeNonEqual(ref, ref.uctx.nullRef)
145-
if (newConstraints.isContradicting || equalityConstraints.isContradicting) {
146-
// the [ref] can't be equal to null
147-
contradiction()
148-
} else {
149-
// Inferring new symbolic disequalities here
150-
for ((key, value) in _symbolicRefToTypeRegion.entries) {
151-
// TODO: cache intersections?
152-
if (key != ref && value.intersect(newConstraints).isEmpty) {
153-
// If we have two inputs of incomparable reference types, then they are non equal
154-
equalityConstraints.makeNonEqual(ref, key)
155-
}
156-
}
157-
setRegion(ref, newConstraints)
158-
}
128+
updateRegionCannotBeEqualNull(ref) { it.excludeSupertype(type) }
159129
}
160130

161131
else -> error("Provided heap ref must be either concrete or purely symbolic one, found $ref")
@@ -165,7 +135,7 @@ class UTypeConstraints<Type>(
165135
/**
166136
* @return a type stream corresponding to the [ref].
167137
*/
168-
internal fun readTypeStream(ref: UHeapRef): UTypeStream<Type> =
138+
internal fun getTypeStream(ref: UHeapRef): UTypeStream<Type> =
169139
when (ref) {
170140
is UConcreteHeapRef -> {
171141
val concreteType = concreteRefToType[ref.address]
@@ -180,15 +150,61 @@ class UTypeConstraints<Type>(
180150
is UNullRef -> error("Null ref should be handled explicitly earlier")
181151

182152
is USymbolicHeapRef -> {
183-
getRegion(ref).typeStream
153+
getTypeRegion(ref).typeStream
184154
}
185155

186156
else -> error("Unexpected ref: $ref")
187157
}
188158

189-
private fun intersectConstraints(ref1: USymbolicHeapRef, ref2: USymbolicHeapRef) {
190-
val newRegion = getRegion(ref1).intersect(getRegion(ref2))
191-
setRegion(ref1, newRegion)
159+
private fun intersectRegions(ref1: USymbolicHeapRef, ref2: USymbolicHeapRef) {
160+
val region = getTypeRegion(ref2)
161+
updateRegionCanBeEqualNull(ref1, region::intersect)
162+
}
163+
164+
protected fun updateRegionCannotBeEqualNull(
165+
ref: USymbolicHeapRef,
166+
regionMapper: (UTypeRegion<Type>) -> UTypeRegion<Type>,
167+
) {
168+
val region = getTypeRegion(ref)
169+
val newRegion = regionMapper(region)
170+
if (newRegion == region) {
171+
return
172+
}
173+
equalityConstraints.makeNonEqual(ref, ref.uctx.nullRef)
174+
if (newRegion.isEmpty || equalityConstraints.isContradicting) {
175+
contradiction()
176+
return
177+
}
178+
for ((key, value) in _symbolicRefToTypeRegion.entries) {
179+
// TODO: cache intersections?
180+
if (key != ref && value.intersect(newRegion).isEmpty) {
181+
// If we have two inputs of incomparable reference types, then they are non equal
182+
equalityConstraints.makeNonEqual(ref, key)
183+
}
184+
}
185+
setTypeRegion(ref, newRegion)
186+
}
187+
188+
protected fun updateRegionCanBeEqualNull(
189+
ref: USymbolicHeapRef,
190+
regionMapper: (UTypeRegion<Type>) -> UTypeRegion<Type>,
191+
) {
192+
val region = getTypeRegion(ref)
193+
val newRegion = regionMapper(region)
194+
if (newRegion == region) {
195+
return
196+
}
197+
if (newRegion.isEmpty) {
198+
equalityConstraints.makeEqual(ref, ref.uctx.nullRef)
199+
}
200+
for ((key, value) in _symbolicRefToTypeRegion.entries) {
201+
// TODO: cache intersections?
202+
if (key != ref && value.intersect(newRegion).isEmpty) {
203+
// If we have two inputs of incomparable reference types, then they are non equal
204+
equalityConstraints.makeNonEqualOrBothNull(ref, key)
205+
}
206+
}
207+
setTypeRegion(ref, newRegion)
192208
}
193209

194210
/**
@@ -209,9 +225,9 @@ class UTypeConstraints<Type>(
209225
// accordingly to the [UIsExpr] specification, [nullRef] always satisfies the [type]
210226
return@mapper symbolicRef.ctx.trueExpr
211227
}
212-
val typeRegion = getRegion(symbolicRef)
228+
val typeRegion = getTypeRegion(symbolicRef)
213229

214-
if (typeRegion.addSupertype(type).isContradicting) {
230+
if (typeRegion.addSupertype(type).isEmpty) {
215231
symbolicRef.ctx.falseExpr
216232
} else {
217233
symbolicRef.uctx.mkIsExpr(symbolicRef, type)

usvm-core/src/main/kotlin/org/usvm/memory/HeapRefSplitting.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ infix fun <T> GuardedExpr<T>.withAlso(guard: UBoolExpr) = GuardedExpr(expr, guar
2626
* @param symbolicHeapRef an ite made of all [USymbolicHeapRef]s with its guard, the single [USymbolicHeapRef] if it's
2727
* the single [USymbolicHeapRef] in the base expression or `null` if there are no [USymbolicHeapRef]s at all.
2828
*/
29-
internal data class SplitHeapRefs(
29+
data class SplitHeapRefs(
3030
val concreteHeapRefs: List<GuardedExpr<UConcreteHeapRef>>,
3131
val symbolicHeapRef: GuardedExpr<UHeapRef>?,
3232
)
@@ -43,7 +43,7 @@ internal data class SplitHeapRefs(
4343
* @param ignoreNullRefs if true, then null references will be ignored. It means that all leafs with nulls
4444
* considered unsatisfiable, so we assume their guards equal to false, and they won't be added to the result.
4545
*/
46-
internal fun splitUHeapRef(
46+
fun splitUHeapRef(
4747
ref: UHeapRef,
4848
initialGuard: UBoolExpr = ref.ctx.trueExpr,
4949
ignoreNullRefs: Boolean = true,
@@ -75,7 +75,7 @@ internal fun splitUHeapRef(
7575
* @param ignoreNullRefs if true, then null references will be ignored. It means that all leafs with nulls
7676
* considered unsatisfiable, so we assume their guards equal to false.
7777
*/
78-
internal inline fun withHeapRef(
78+
inline fun withHeapRef(
7979
ref: UHeapRef,
8080
initialGuard: UBoolExpr,
8181
crossinline blockOnConcrete: (GuardedExpr<UConcreteHeapRef>) -> Unit,
@@ -104,9 +104,9 @@ internal inline fun withHeapRef(
104104
}
105105
}
106106

107-
private const val LEFT_CHILD = 0
108-
private const val RIGHT_CHILD = 1
109-
private const val DONE = 2
107+
const val LEFT_CHILD = 0
108+
const val RIGHT_CHILD = 1
109+
const val DONE = 2
110110

111111

112112
/**
@@ -118,7 +118,7 @@ private const val DONE = 2
118118
* considered unsatisfiable, so we assume their guards equal to false. If [ignoreNullRefs] is true and [this] is
119119
* [UNullRef], throws an [IllegalArgumentException].
120120
*/
121-
internal inline fun <Sort : USort> UHeapRef.map(
121+
inline fun <Sort : USort> UHeapRef.map(
122122
crossinline concreteMapper: (UConcreteHeapRef) -> UExpr<Sort>,
123123
crossinline symbolicMapper: (USymbolicHeapRef) -> UExpr<Sort>,
124124
ignoreNullRefs: Boolean = true,

usvm-core/src/main/kotlin/org/usvm/memory/Memory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,5 @@ open class UMemoryBase<Field, Type, Method>(
162162
UMemoryBase(ctx, typeConstraints, stack.clone(), heap.toMutableHeap(), mocker)
163163

164164
override fun typeStreamOf(ref: UHeapRef): UTypeStream<Type> =
165-
types.readTypeStream(ref)
165+
types.getTypeStream(ref)
166166
}

usvm-core/src/main/kotlin/org/usvm/model/LazyModels.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class ULazyHeapModel<Field, ArrayType>(
117117
// All the expressions in the model are interpreted, therefore, they must
118118
// have concrete addresses. Moreover, the model knows only about input values
119119
// which have addresses less or equal than INITIAL_INPUT_ADDRESS
120-
require(ref is UConcreteHeapRef && ref.address <= INITIAL_INPUT_ADDRESS)
120+
require(ref is UConcreteHeapRef && ref.address <= INITIAL_INPUT_ADDRESS) { "Unexpected ref: $ref" }
121121

122122
val resolvedRegion = resolvedInputFields[field]
123123
val regionId = UInputFieldId(field, sort, contextHeap = null)
@@ -142,7 +142,7 @@ class ULazyHeapModel<Field, ArrayType>(
142142
// All the expressions in the model are interpreted, therefore, they must
143143
// have concrete addresses. Moreover, the model knows only about input values
144144
// which have addresses less or equal than INITIAL_INPUT_ADDRESS
145-
require(ref is UConcreteHeapRef && ref.address <= INITIAL_INPUT_ADDRESS)
145+
require(ref is UConcreteHeapRef && ref.address <= INITIAL_INPUT_ADDRESS) { "Unexpected ref: $ref" }
146146

147147
val key = ref to index
148148

@@ -164,7 +164,7 @@ class ULazyHeapModel<Field, ArrayType>(
164164
// All the expressions in the model are interpreted, therefore, they must
165165
// have concrete addresses. Moreover, the model knows only about input values
166166
// which have addresses less or equal than INITIAL_INPUT_ADDRESS
167-
require(ref is UConcreteHeapRef && ref.address <= INITIAL_INPUT_ADDRESS)
167+
require(ref is UConcreteHeapRef && ref.address <= INITIAL_INPUT_ADDRESS) { "Unexpected ref: $ref" }
168168

169169
val resolvedRegion = resolvedInputLengths[arrayType]
170170
val regionId = UInputArrayLengthId(arrayType, ref.uctx.sizeSort, contextHeap = null)

usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class USoftConstraintsProvider<Field, Type>(override val ctx: UContext) : UTrans
5252
fun provide(initialExpr: UExpr<*>): Set<UBoolExpr> =
5353
caches.getOrElse(initialExpr) {
5454
apply(initialExpr)
55-
provide(initialExpr)
55+
caches.getValue(initialExpr)
5656
}
5757

5858
// region The most common methods
@@ -97,10 +97,9 @@ class USoftConstraintsProvider<Field, Type>(override val ctx: UContext) : UTrans
9797
expr: UConcreteHeapRef,
9898
): UExpr<UAddressSort> = error("Illegal operation since UConcreteHeapRef must not be translated into a solver")
9999

100-
override fun transform(expr: UNullRef): UExpr<UAddressSort> = expr
100+
override fun transform(expr: UNullRef): UExpr<UAddressSort> = transformExpr(expr)
101101

102-
override fun transform(expr: UIsExpr<Type>): UBoolExpr =
103-
error("Illegal operation since UIsExpr should not be translated into a SMT solver")
102+
override fun transform(expr: UIsExpr<Type>): UBoolExpr = transformExpr(expr)
104103

105104
override fun transform(
106105
expr: UInputArrayLengthReading<Type>,

usvm-core/src/main/kotlin/org/usvm/types/TypeRegion.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ open class UTypeRegion<Type>(
1616
val subtypes: PersistentSet<Type> = persistentSetOf(),
1717
val notSubtypes: PersistentSet<Type> = persistentSetOf(),
1818
) : Region<UTypeRegion<Type>> {
19-
val isContradicting get() = typeStream.isEmpty
20-
2119
/**
2220
* Returns region that represents empty set of types. Called when type
2321
* constraints contradict, for example if X <: Y and X </: Y.
@@ -37,7 +35,7 @@ open class UTypeRegion<Type>(
3735
* - t is final && t </: X && X <: t
3836
*/
3937
open fun addSupertype(supertype: Type): UTypeRegion<Type> {
40-
if (isContradicting || supertypes.any { typeSystem.isSupertype(supertype, it) }) {
38+
if (isEmpty || supertypes.any { typeSystem.isSupertype(supertype, it) }) {
4139
return this
4240
}
4341

@@ -84,7 +82,7 @@ open class UTypeRegion<Type>(
8482
* X <: u && u <: t && X </: t, i.e. if [supertypes] contains subtype of [notSupertype]
8583
*/
8684
open fun excludeSupertype(notSupertype: Type): UTypeRegion<Type> {
87-
if (isContradicting || notSupertypes.any { typeSystem.isSupertype(it, notSupertype) }) {
85+
if (isEmpty || notSupertypes.any { typeSystem.isSupertype(it, notSupertype) }) {
8886
return this
8987
}
9088

@@ -108,8 +106,8 @@ open class UTypeRegion<Type>(
108106
* - t <: X && u <: t && u </: X, i.e. if [notSubtypes] contains subtype of [subtype]
109107
* - t <: X && X <: u && t </: u
110108
*/
111-
protected open fun addSubtype(subtype: Type): UTypeRegion<Type> {
112-
if (isContradicting || subtypes.any { typeSystem.isSupertype(it, subtype) }) {
109+
open fun addSubtype(subtype: Type): UTypeRegion<Type> {
110+
if (isEmpty || subtypes.any { typeSystem.isSupertype(it, subtype) }) {
113111
return this
114112
}
115113

@@ -134,8 +132,8 @@ open class UTypeRegion<Type>(
134132
* - u <: X && t <: u && t </: X, i.e. if [subtypes] contains supertype of [notSubtype]
135133
* - t is final && t </: X && X <: t
136134
*/
137-
protected open fun excludeSubtype(notSubtype: Type): UTypeRegion<Type> {
138-
if (isContradicting || notSubtypes.any { typeSystem.isSupertype(notSubtype, it) }) {
135+
open fun excludeSubtype(notSubtype: Type): UTypeRegion<Type> {
136+
if (isEmpty || notSubtypes.any { typeSystem.isSupertype(notSubtype, it) }) {
139137
return this
140138
}
141139

@@ -153,9 +151,12 @@ open class UTypeRegion<Type>(
153151
return UTypeRegion(typeSystem, newTypeStream, notSubtypes = newNotSubtypes)
154152
}
155153

156-
override val isEmpty: Boolean = isContradicting
154+
override val isEmpty: Boolean = typeStream.isEmpty
157155

158156
override fun intersect(other: UTypeRegion<Type>): UTypeRegion<Type> {
157+
if (this == other) {
158+
return this
159+
}
159160
// TODO: optimize things up by not re-allocating type regions after each operation
160161
val otherSize = other.size
161162
val thisSize = this.size
@@ -164,6 +165,9 @@ open class UTypeRegion<Type>(
164165
} else {
165166
this to other
166167
}
168+
if (smallRegion.isEmpty) {
169+
return smallRegion
170+
}
167171

168172
val result1 = smallRegion.supertypes.fold(largeRegion) { acc, t -> acc.addSupertype(t) }
169173
val result2 = smallRegion.notSupertypes.fold(result1) { acc, t -> acc.excludeSupertype(t) }
@@ -172,6 +176,9 @@ open class UTypeRegion<Type>(
172176
}
173177

174178
override fun subtract(other: UTypeRegion<Type>): UTypeRegion<Type> {
179+
if (isEmpty || other.isEmpty) {
180+
return this
181+
}
175182
if (other.notSupertypes.isNotEmpty() || other.notSubtypes.isEmpty() || other.supertypes.size + other.subtypes.size != 1) {
176183
TODO("For now, we are able to subtract only positive singleton type constraints")
177184
}

usvm-core/src/test/kotlin/org/usvm/types/TypeSolverTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,29 @@ class TypeSolverTest {
148148
assertIs<UUnsatResult<UModelBase<Field, TestType>>>(resultWithEqConstraint)
149149
}
150150

151+
@Test
152+
fun `Test symbolic ref -- different types 3 refs`(): Unit = with(ctx) {
153+
val ref0 = ctx.mkRegisterReading(0, addressSort)
154+
val ref1 = ctx.mkRegisterReading(1, addressSort)
155+
val ref2 = ctx.mkRegisterReading(2, addressSort)
156+
157+
pc += mkIsExpr(ref0, interfaceAB)
158+
pc += mkIsExpr(ref1, interfaceBC1)
159+
pc += mkIsExpr(ref2, interfaceAC)
160+
pc += mkHeapRefEq(ref0, nullRef).not()
161+
pc += mkHeapRefEq(ref1, nullRef).not()
162+
pc += mkHeapRefEq(ref2, nullRef).not()
163+
164+
val resultWithoutEqConstraint = solver.check(pc)
165+
val model = assertIs<USatResult<UModelBase<Field, TestType>>>(resultWithoutEqConstraint).model
166+
assertNotEquals(model.eval(ref0), model.eval(ref1))
167+
168+
pc += mkHeapRefEq(ref0, ref1)
169+
pc += mkHeapRefEq(ref1, ref2)
170+
171+
assertTrue(pc.isFalse)
172+
}
173+
151174
@Test
152175
fun `Test symbolic ref -- different interface types but same base type`(): Unit = with(ctx) {
153176
val ref0 = ctx.mkRegisterReading(0, addressSort)

0 commit comments

Comments
 (0)