Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Undefined and null values support #248

Merged
merged 15 commits into from
Feb 12, 2025
4 changes: 4 additions & 0 deletions usvm-ts/src/main/kotlin/org/usvm/api/TSTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ sealed interface TSObject {
data class TSObject(val addr: Int) : org.usvm.api.TSObject

data object TSUnknown : org.usvm.api.TSObject

data object TSNull : org.usvm.api.TSObject

data object TSException : org.usvm.api.TSObject
}
7 changes: 7 additions & 0 deletions usvm-ts/src/main/kotlin/org/usvm/machine/TSContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package org.usvm.machine

import io.ksmt.sort.KFp64Sort
import org.jacodb.ets.base.EtsBooleanType
import org.jacodb.ets.base.EtsNullType
import org.jacodb.ets.base.EtsNumberType
import org.jacodb.ets.base.EtsRefType
import org.jacodb.ets.base.EtsType
import org.jacodb.ets.base.EtsUndefinedType
import org.jacodb.ets.base.EtsUnknownType
import org.jacodb.ets.model.EtsScene
import org.usvm.UAddressSort
Expand Down Expand Up @@ -41,6 +43,8 @@ class TSContext(
is EtsBooleanType -> boolSort
is EtsNumberType -> fp64Sort
is EtsRefType -> addressSort
is EtsNullType -> addressSort
is EtsUndefinedType -> addressSort
is EtsUnknownType -> unresolvedSort
else -> TODO("Support all JacoDB types, encountered $type")
}
Expand All @@ -61,6 +65,9 @@ class TSContext(

fun mkUndefinedValue(): UExpr<UAddressSort> = undefinedValue

fun mkTSNullValue(): UConcreteHeapRef = nullValue
private val nullValue: UConcreteHeapRef = mkConcreteHeapRef(addressCounter.freshStaticAddress())

fun getIntermediateBoolLValue(addr: Int): UFieldLValue<IntermediateLValueField, UBoolSort> {
return UFieldLValue(boolSort, mkConcreteHeapRef(addr), IntermediateLValueField.BOOL)
}
Expand Down
34 changes: 28 additions & 6 deletions usvm-ts/src/main/kotlin/org/usvm/machine/expr/TSExprResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.jacodb.ets.base.EtsNotEqExpr
import org.jacodb.ets.base.EtsNotExpr
import org.jacodb.ets.base.EtsNullConstant
import org.jacodb.ets.base.EtsNullType

Check warning

Code scanning / detekt

Detects unused imports Warning

Unused import
import org.jacodb.ets.base.EtsNullishCoalescingExpr
import org.jacodb.ets.base.EtsNumberConstant
import org.jacodb.ets.base.EtsNumberType
Expand All @@ -55,6 +56,7 @@
import org.jacodb.ets.base.EtsStrictEqExpr
import org.jacodb.ets.base.EtsStrictNotEqExpr
import org.jacodb.ets.base.EtsStringConstant
import org.jacodb.ets.base.EtsStringType
import org.jacodb.ets.base.EtsSubExpr
import org.jacodb.ets.base.EtsTernaryExpr
import org.jacodb.ets.base.EtsThis
Expand All @@ -72,6 +74,7 @@
import org.usvm.UAddressSort
import org.usvm.UBoolSort
import org.usvm.UExpr
import org.usvm.UHeapRef
import org.usvm.USort
import org.usvm.api.makeSymbolicPrimitive
import org.usvm.collection.field.UFieldLValue
Expand All @@ -80,13 +83,15 @@
import org.usvm.machine.operator.TSBinaryOperator
import org.usvm.machine.operator.TSUnaryOperator
import org.usvm.machine.state.TSMethodResult
import org.usvm.machine.state.TSState
import org.usvm.machine.state.localsCount
import org.usvm.machine.state.newStmt
import org.usvm.machine.types.FakeType
import org.usvm.machine.types.mkFakeValue
import org.usvm.memory.ULValue
import org.usvm.memory.URegisterStackLValue
import org.usvm.util.fieldLookUp
import org.usvm.util.throwExceptionWithoutStackFrameDrop

private val logger = KotlinLogging.logger {}

Expand Down Expand Up @@ -354,8 +359,7 @@
}

override fun visit(expr: EtsStrictEqExpr): UExpr<out USort>? {
logger.warn { "visit(${expr::class.simpleName}) is not implemented yet" }
error("Not supported $expr")
return resolveBinaryOperator(TSBinaryOperator.StrictEq, expr)
}

override fun visit(expr: EtsStrictNotEqExpr): UExpr<out USort>? {
Expand Down Expand Up @@ -499,8 +503,28 @@
error("Not supported $value")
}

private fun checkUndefinedOrNullPropertyRead(instance: UHeapRef) = with(ctx) {
val neqNull = mkAnd(
mkHeapRefEq(instance, ctx.mkUndefinedValue()).not(),
mkHeapRefEq(instance, ctx.mkTSNullValue()).not()
)

scope.fork(
neqNull,
blockOnFalseState = allocateException(EtsStringType) // TODO incorrect exception type
)
}

private fun allocateException(type: EtsType): (TSState) -> Unit = { state ->
val address = state.memory.allocConcrete(type)
state.throwExceptionWithoutStackFrameDrop(address, type)
}

override fun visit(value: EtsInstanceFieldRef): UExpr<out USort>? = with(ctx) {
val instanceRef = resolve(value.instance)?.asExpr(addressSort) ?: return null

checkUndefinedOrNullPropertyRead(instanceRef) ?: return null

// TODO: checkNullPointer(instanceRef)
val fieldType = scene.fieldLookUp(value.field).type
val sort = typeToSort(fieldType)
Expand Down Expand Up @@ -639,13 +663,11 @@
}

override fun visit(value: EtsNullConstant): UExpr<out USort> = with(ctx) {
// TODO: replace with `ctx.nullConstant` (!= nullRef)
nullRef
mkTSNullValue()
}

override fun visit(value: EtsUndefinedConstant): UExpr<out USort> = with(ctx) {
// TODO: replace with `ctx.nullRef` or `ctx.undefinedConstant` (== nullRef)
nullRef
mkUndefinedValue()
}

override fun visit(value: EtsArrayAccess): UExpr<out USort> = with(ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.jacodb.ets.base.EtsGotoStmt
import org.jacodb.ets.base.EtsIfStmt
import org.jacodb.ets.base.EtsLocal
import org.jacodb.ets.base.EtsNopStmt
import org.jacodb.ets.base.EtsNullType
import org.jacodb.ets.base.EtsParameterRef
import org.jacodb.ets.base.EtsReturnStmt
import org.jacodb.ets.base.EtsStmt
Expand Down Expand Up @@ -208,6 +209,8 @@ class TSInterpreter(
state.memory.stack.push(method.parametersWithThisCount, method.localsCount)
state.newStmt(method.cfg.instructions.first())

state.memory.types.allocate(ctx.mkTSNullValue().address, EtsNullType)

return state
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ sealed interface TSBinaryOperator {
)
)

scope.assert(lhsType.fpTypeExpr or lhsType.boolTypeExpr)
// TODO: support objects
}

Expand All @@ -206,6 +207,7 @@ sealed interface TSBinaryOperator {
)
)

scope.assert(lhsType.fpTypeExpr or lhsType.boolTypeExpr)
// TODO: support objects
}

Expand All @@ -218,6 +220,7 @@ sealed interface TSBinaryOperator {
)
)

scope.assert(lhsType.refTypeExpr)
// TODO: support objects
}

Expand Down Expand Up @@ -250,6 +253,7 @@ sealed interface TSBinaryOperator {
)
)

scope.assert(rhsType.fpTypeExpr or rhsType.boolTypeExpr)
// TODO: support objects
}

Expand All @@ -270,6 +274,7 @@ sealed interface TSBinaryOperator {
)
)

scope.assert(rhsType.fpTypeExpr or rhsType.boolTypeExpr)
// TODO: support objects
}

Expand All @@ -282,6 +287,7 @@ sealed interface TSBinaryOperator {
)
)

scope.assert(rhsType.refTypeExpr)
// TODO: support objects

}
Expand Down Expand Up @@ -330,7 +336,6 @@ sealed interface TSBinaryOperator {

TODO("Unsupported String and bigint comparison")
}

}

data object Neq : TSBinaryOperator {
Expand Down Expand Up @@ -385,6 +390,52 @@ sealed interface TSBinaryOperator {
}
}

data object StrictEq : TSBinaryOperator {
override fun TSContext.onBool(
lhs: UExpr<UBoolSort>,
rhs: UExpr<UBoolSort>,
scope: TSStepScope,
): UExpr<out USort> {
return mkEq(lhs, rhs)
}

override fun TSContext.onFp(
lhs: UExpr<KFp64Sort>,
rhs: UExpr<KFp64Sort>,
scope: TSStepScope,
): UExpr<out USort> {
return mkFpEqualExpr(lhs, rhs)
}

override fun TSContext.onRef(
lhs: UExpr<UAddressSort>,
rhs: UExpr<UAddressSort>,
scope: TSStepScope,
): UExpr<out USort> {
TODO("Not yet implemented")
}

override fun TSContext.resolveFakeObject(
lhs: UExpr<out USort>,
rhs: UExpr<out USort>,
scope: TSStepScope,
): UExpr<out USort> {
check(lhs.isFakeObject() || rhs.isFakeObject())
// TODO: delegating to '==' is not correct in general case
return with(Eq) {
resolveFakeObject(lhs, rhs, scope)
}
}

override fun TSContext.internalResolve(
lhs: UExpr<out USort>,
rhs: UExpr<out USort>,
scope: TSStepScope,
): UExpr<out USort> {
TODO("Not yet implemented")
}
}

data object Add : TSBinaryOperator {
override fun TSContext.onBool(
lhs: UExpr<UBoolSort>,
Expand Down
10 changes: 9 additions & 1 deletion usvm-ts/src/main/kotlin/org/usvm/util/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package org.usvm.util

import io.ksmt.sort.KFp64Sort
import org.jacodb.ets.base.EtsType
import org.jacodb.ets.model.EtsFieldSignature
import org.jacodb.ets.model.EtsScene
import org.usvm.UBoolSort
import org.usvm.UExpr
import org.usvm.UHeapRef
import org.usvm.machine.TSContext
import org.usvm.machine.state.TSMethodResult
import org.usvm.machine.state.TSState

// Built-in KContext.bvToBool has identical implementation.
fun TSContext.boolToFp(expr: UExpr<UBoolSort>): UExpr<KFp64Sort> =
Expand All @@ -16,4 +20,8 @@ fun TSContext.boolToFp(expr: UExpr<UBoolSort>): UExpr<KFp64Sort> =
fun EtsScene.fieldLookUp(field: EtsFieldSignature) = projectAndSdkClasses
.first { it.signature == field.enclosingClass }
.fields
.single { it.name == field.name }
.single { it.name == field.name }

fun TSState.throwExceptionWithoutStackFrameDrop(address: UHeapRef, type: EtsType) {
methodResult = TSMethodResult.TSException(address, type)
}
25 changes: 22 additions & 3 deletions usvm-ts/src/test/kotlin/org/usvm/samples/InstanceFields.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,65 @@ class InstanceFields : TSMethodTestRunner() {
@Test
fun testReturnSingleField() {
val method = getMethod("InstanceFields", "returnSingleField")
discoverProperties<TSObject.TSClass, TSObject.TSNumber>(
discoverProperties<TSObject, TSObject>(
method,
{ x, r ->
// Note: this is an attempt to represent `r == x["a"]`
if (x !is TSObject.TSClass || r !is TSObject.TSNumber) return@discoverProperties false

val xa = x.properties["a"] as TSObject.TSNumber
xa.number == r.number || (xa.number.isNaN() && r.number.isNaN())
},
{ x, r ->
(x is TSObject.TSUndefinedObject || x is TSObject.TSNull) && r is TSObject.TSException
}
)
}

@Test
fun testDispatchOverField() {
val method = getMethod("InstanceFields", "dispatchOverField")
discoverProperties<TSObject.TSClass, TSObject.TSNumber>(
discoverProperties<TSObject, TSObject>(
method,
{ x, r ->
if (x !is TSObject.TSClass || r !is TSObject.TSNumber) return@discoverProperties false

val xa = x.properties["a"] as TSObject.TSNumber
xa.number == 1.0 && r.number == 1.0
},
{ x, r ->
if (x !is TSObject.TSClass || r !is TSObject.TSNumber) return@discoverProperties false

val xa = x.properties["a"] as TSObject.TSNumber
xa.number == 2.0 && r.number == 2.0
},
{ x, r ->
if (x !is TSObject.TSClass || r !is TSObject.TSNumber) return@discoverProperties false

val xa = x.properties["a"] as TSObject.TSNumber
xa.number != 1.0 && xa.number != 2.0 && r.number == 100.0
},
{ x, r ->
(x is TSObject.TSUndefinedObject || x is TSObject.TSNull) && r is TSObject.TSException
}
)
}

@Test
fun testReturnSumOfTwoFields() {
val method = getMethod("InstanceFields", "returnSumOfTwoFields")
discoverProperties<TSObject.TSClass, TSObject.TSNumber>(
discoverProperties<TSObject, TSObject>(
method,
{ x, r ->
if (x !is TSObject.TSClass || r !is TSObject.TSNumber) return@discoverProperties false

val xa = x.properties["a"] as TSObject.TSNumber
val xb = x.properties["b"] as TSObject.TSNumber
xa.number + xb.number == r.number
},
{ x, r ->
(x is TSObject.TSUndefinedObject || x is TSObject.TSNull) && r is TSObject.TSException
}
)
}
}
28 changes: 28 additions & 0 deletions usvm-ts/src/test/kotlin/org/usvm/samples/Null.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.usvm.samples

import org.jacodb.ets.model.EtsScene
import org.jacodb.ets.utils.loadEtsFileAutoConvert
import org.junit.jupiter.api.RepeatedTest
import org.usvm.api.TSObject
import org.usvm.util.TSMethodTestRunner
import org.usvm.util.getResourcePath

class Null : TSMethodTestRunner() {

override val scene = run {
val name = "Null.ts"
val path = getResourcePath("/samples/$name")
val file = loadEtsFileAutoConvert(path)
EtsScene(listOf(file))
}

@RepeatedTest(20)
fun testIsNull() {
val method = getMethod("Null", "isNull")
discoverProperties<TSObject, TSObject.TSNumber>(
method,
{ a, r -> a is TSObject.TSNull && r.number == 1.0 },
{ a, r -> a !is TSObject.TSNull && r.number == 2.0 },
)
}
}
Loading
Loading