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

Basic USVM TS type system with type coercion #215

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fe1d03b
TSTypeSystem + TSTopTypeStream implementation
zishkaz Aug 12, 2024
471d31d
Implement basic type coercion
zishkaz Aug 22, 2024
bcb0c1e
Type coercion tests working
zishkaz Aug 27, 2024
344de3f
Add type coercion test
zishkaz Aug 30, 2024
08178af
Dev sync
zishkaz Sep 2, 2024
c3082ee
Dev sync 2
zishkaz Sep 4, 2024
5015c31
TSTypeSystem + TSTopTypeStream implementation
zishkaz Aug 12, 2024
8808008
Implement basic type coercion
zishkaz Aug 22, 2024
7984e94
Type coercion tests working
zishkaz Aug 27, 2024
aa5dccc
Add type coercion test
zishkaz Aug 30, 2024
83f848f
Dev sync
zishkaz Sep 2, 2024
f297099
Dev sync 2
zishkaz Sep 4, 2024
fc54035
Merge remote-tracking branch 'origin/sergeyl/ts_typecoercion' into se…
zishkaz Sep 4, 2024
b4ea3ae
Correct transitiveCoercionNoTypes test (needs cleanup)
zishkaz Sep 11, 2024
6cc6036
Hotfix
zishkaz Sep 12, 2024
064703f
Minor refactoring
zishkaz Sep 12, 2024
7470894
Support transitiveCoercionNoTypes test
zishkaz Sep 18, 2024
bb320ac
Refactoring + code documentation
zishkaz Sep 23, 2024
f87cd38
Merge branch 'refs/heads/main' into sergeyl/ts_typecoercion
zishkaz Sep 23, 2024
6abe719
Post-merge fix
zishkaz Sep 23, 2024
cbd2070
boolToFpSort bug fix
zishkaz Sep 27, 2024
2db7013
Minor refactoring
zishkaz Sep 30, 2024
54ebc12
JAcoDB version update
zishkaz Sep 30, 2024
942af48
Minor fix
zishkaz Oct 2, 2024
bc8e331
Run CI with --scan gradle flag
zishkaz Oct 2, 2024
b49f420
Update arkanalyzer version in CI
zishkaz Oct 2, 2024
ce54887
Revert --scan changes
zishkaz Oct 2, 2024
6e81b3c
Set arkanalyzer version the same as jacodb/neo
zishkaz Oct 7, 2024
1dc45cf
Extract common API + comment
zishkaz Oct 7, 2024
8bf5943
Fix comment
zishkaz Oct 7, 2024
7e7cc1c
Refactoring + code documentation
zishkaz Oct 8, 2024
a75a753
TSTypeStorage commentary
zishkaz Oct 8, 2024
9b7b106
Detekt fix + code documentation
zishkaz Oct 8, 2024
e1d755c
Hotfix
zishkaz Oct 8, 2024
b55b3e4
Disable tests
zishkaz Oct 8, 2024
e85df2b
Remove TSTypeStorage
zishkaz Oct 17, 2024
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
10 changes: 5 additions & 5 deletions .github/workflows/build-and-run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,27 @@ jobs:
- name: Set up ArkAnalyzer
run: |
REPO_URL="https://gitee.com/Lipenx/arkanalyzer.git"
DEST_DIR="arkanalyzer"
DEST_DIR="arkanalyzer"
MAX_RETRIES=10
RETRY_DELAY=3 # Delay between retries in seconds
BRANCH="neo/2024-08-07"
zishkaz marked this conversation as resolved.
Show resolved Hide resolved
BRANCH="neo/2024-08-16" # Set the same as in jacodb/neo branch, since we get jacodb artifact from that branch.

for ((i=1; i<=MAX_RETRIES; i++)); do
git clone --depth=1 --branch $BRANCH $REPO_URL $DEST_DIR && break
echo "Clone failed, retrying in $RETRY_DELAY seconds..."
sleep "$RETRY_DELAY"
done

if [[ $i -gt $MAX_RETRIES ]]; then
echo "Failed to clone the repository after $MAX_RETRIES attempts."
exit 1
else
echo "Repository cloned successfully."
fi

echo "ARKANALYZER_DIR=$(realpath $DEST_DIR)" >> $GITHUB_ENV
cd $DEST_DIR

npm install
npm run build

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.gradle.plugin.use.PluginDependenciesSpec
object Versions {
const val detekt = "1.18.1"
const val ini4j = "0.5.4"
const val jacodb = "ae2716b3f8"
const val jacodb = "3377c0cb88"
const val juliet = "1.3.2"
const val junit = "5.9.3"
const val kotlin = "1.9.20"
Expand Down
44 changes: 44 additions & 0 deletions usvm-core/src/main/kotlin/org/usvm/Expressions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,53 @@ class UIsSupertypeExpr<Type> internal constructor(

//endregion

//region Utility Expressions

/**
* Utility class for merging expressions with [UBoolSort] sort.
*
* Mainly created for [not] function used in StateForker.
*/
class UJoinedBoolExpr(
ctx: UContext<*>,
val exprs: List<UBoolExpr>,
) : UBoolExpr(ctx) {
override val sort: UBoolSort
get() = ctx.boolSort

private val joinedExprs = ctx.mkAnd(exprs)

// Size of exprs is not big since it generates from all sorts supported by machine [n]
// (small number even when finished)
// plus possible additional constraints which are C(n - 1, 2) in size,
// so no need to cache this value as its use is also limited.
fun not(): UBoolExpr = ctx.mkAnd(exprs.map(ctx::mkNot))

override fun accept(transformer: KTransformerBase): KExpr<UBoolSort> {
return transformer.apply(joinedExprs)
}

// TODO: draft
override fun internEquals(other: Any): Boolean = structurallyEqual(other)
Comment on lines +344 to +345
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it a draft? Seems like we can implement it


// TODO: draft
override fun internHashCode(): Int = hash()

override fun print(printer: ExpressionPrinter) {
printer.append("joined(")
joinedExprs.print(printer)
printer.append(")")
}
}

//endregion

//region Utils

val UBoolExpr.isFalse get() = this == ctx.falseExpr
val UBoolExpr.isTrue get() = this == ctx.trueExpr

fun UExpr<*>.unwrapJoinedExpr(ctx: UContext<*>): UExpr<out USort> =
if (this is UJoinedBoolExpr) ctx.mkAnd(exprs) else this

//endregion
12 changes: 7 additions & 5 deletions usvm-core/src/main/kotlin/org/usvm/StateForker.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.usvm

import io.ksmt.utils.cast
import org.usvm.collections.immutable.internal.MutabilityOwnership
import org.usvm.model.UModelBase
import org.usvm.solver.USatResult
Expand Down Expand Up @@ -45,9 +46,10 @@ object WithSolverStateForker : StateForker {
state: T,
condition: UBoolExpr,
): ForkResult<T> {
val (trueModels, falseModels, _) = splitModelsByCondition(state.models, condition)
val unwrappedCondition: UBoolExpr = condition.unwrapJoinedExpr(state.ctx).cast()
val (trueModels, falseModels, _) = splitModelsByCondition(state.models, unwrappedCondition)

val notCondition = state.ctx.mkNot(condition)
val notCondition = if (condition is UJoinedBoolExpr) condition.not() else state.ctx.mkNot(unwrappedCondition)
val (posState, negState) = when {

trueModels.isNotEmpty() && falseModels.isNotEmpty() -> {
Expand All @@ -56,23 +58,23 @@ object WithSolverStateForker : StateForker {

posState.models = trueModels
negState.models = falseModels
posState.pathConstraints += condition
posState.pathConstraints += unwrappedCondition
negState.pathConstraints += notCondition

posState to negState
}

trueModels.isNotEmpty() -> state to forkIfSat(
state,
newConstraintToOriginalState = condition,
newConstraintToOriginalState = unwrappedCondition,
newConstraintToForkedState = notCondition,
stateToCheck = ForkedState
)

falseModels.isNotEmpty() -> {
val forkedState = forkIfSat(
state,
newConstraintToOriginalState = condition,
newConstraintToOriginalState = unwrappedCondition,
newConstraintToForkedState = notCondition,
stateToCheck = OriginalState
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.usvm.test.util.checkers.eq


internal class LongWrapperTest : JavaMethodTestRunner() {
@Disabled("Fails")
@Test
fun primitiveToWrapperTest() {
// todo: investigate why only BFS works
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.usvm.test.util.checkers.eq


internal class ShortWrapperTest : JavaMethodTestRunner() {
@Disabled("Fails")
@Test
fun primitiveToWrapperTest() {
// todo: investigate why only BFS works
Expand Down
4 changes: 2 additions & 2 deletions usvm-ts/src/main/kotlin/org/usvm/TSApplicationGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package org.usvm

import org.jacodb.ets.base.EtsStmt
import org.jacodb.ets.graph.EtsApplicationGraphImpl
import org.jacodb.ets.model.EtsFile
import org.jacodb.ets.model.EtsMethod
import org.jacodb.ets.model.EtsScene
import org.usvm.statistics.ApplicationGraph

class TSApplicationGraph(project: EtsFile) : ApplicationGraph<EtsMethod, EtsStmt> {
class TSApplicationGraph(project: EtsScene) : ApplicationGraph<EtsMethod, EtsStmt> {
private val applicationGraph = EtsApplicationGraphImpl(project)

override fun predecessors(node: EtsStmt): Sequence<EtsStmt> =
Expand Down
111 changes: 104 additions & 7 deletions usvm-ts/src/main/kotlin/org/usvm/TSBinaryOperator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,133 @@ package org.usvm

import io.ksmt.utils.cast

/**
* @param[desiredSort] accepts two [USort] instances of the expression operands.
* It defines a desired [USort] for the binary operator to cast both of its operands to.
*
* @param[banSorts] accepts two [UExpr] instances of the expression operands.
* It returns a [Set] of [USort] that are restricted to be coerced to.
*/

// TODO: desiredSort and banSorts achieve the same goal, although have different semantics. Possible to merge them.
sealed class TSBinaryOperator(
val onBool: TSContext.(UExpr<UBoolSort>, UExpr<UBoolSort>) -> UExpr<out USort> = shouldNotBeCalled,
val onBv: TSContext.(UExpr<UBvSort>, UExpr<UBvSort>) -> UExpr<out USort> = shouldNotBeCalled,
val onFp: TSContext.(UExpr<UFpSort>, UExpr<UFpSort>) -> UExpr<out USort> = shouldNotBeCalled,
val onRef: TSContext.(UExpr<UAddressSort>, UExpr<UAddressSort>) -> UExpr<out USort> = shouldNotBeCalled,
zishkaz marked this conversation as resolved.
Show resolved Hide resolved
// Some binary operations like '==' and '!=' can operate on any pair of equal sorts.
// However, '+' casts both operands to Number in TypeScript (no considering string currently),
// so fp64sort is required for both sides.
// This function allows to filter out excess expressions in type coercion.
val desiredSort: TSContext.(USort, USort) -> USort = { _, _ -> error("Should not be called") },
// This function specifies a set of banned sorts pre-coercion.
// Usage of it is limited and was introduced for Neq operation.
// Generally designed to filter out excess expressions in type coercion.
val banSorts: TSContext.(UExpr<out USort>, UExpr<out USort>) -> Set<USort> = { _, _ -> emptySet() },
) {

object Eq : TSBinaryOperator(
onBool = UContext<TSSizeSort>::mkEq,
onBv = UContext<TSSizeSort>::mkEq,
onFp = UContext<TSSizeSort>::mkFpEqualExpr,
onRef = UContext<TSSizeSort>::mkHeapRefEq,
desiredSort = { lhs, _ -> lhs }
)

// Neq must not be applied to a pair of expressions
// containing generated ones during coercion initialization (exprCache intersection).
// For example,
// "a (ref reg reading) != 1.0 (fp64 number)"
// can't yield a list of type coercion bool expressions containing:
// "a (bool reg reading) != true (bool)",
// since "1.0.toBool() = true" is a new value for TSExprTransformer(1.0) exprCache.
//
// So, that's the reason why banSorts in Neq throws out all primitive types except one of the expressions' one.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand what it means

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enriched explanation

// (because obviously we must be able to coerce to expression's base sort)

// TODO: banSorts is still draft here, it only handles specific operands' configurations. General solution required.
object Neq : TSBinaryOperator(
onBool = { lhs, rhs -> lhs.neq(rhs) },
onBv = { lhs, rhs -> lhs.neq(rhs) },
onFp = { lhs, rhs -> mkFpEqualExpr(lhs, rhs).not() },
onRef = { lhs, rhs -> mkHeapRefEq(lhs, rhs).not() },
desiredSort = { lhs, _ -> lhs },
banSorts = { lhs, rhs ->
when {
lhs is TSWrappedValue ->
// rhs.sort == addressSort is a mock not to cause undefined
// behaviour with support of new language features.
// For example, supporting language structures could produce
// incorrect additional sort constraints here if addressSort expressions
// do not return empty set.
if (rhs is TSWrappedValue || rhs.sort == addressSort) {
emptySet()
} else {
TSTypeSystem.primitiveTypes
.map(::typeToSort).toSet()
.minus(rhs.sort)
}
rhs is TSWrappedValue ->
// lhs.sort == addressSort explained as above.
if (lhs.sort == addressSort) {
emptySet()
} else {
TSTypeSystem.primitiveTypes
.map(::typeToSort).toSet()
.minus(lhs.sort)
}
else -> emptySet()
}
}
)

object Add : TSBinaryOperator(
onFp = { lhs, rhs -> mkFpAddExpr(fpRoundingModeSortDefaultValue(), lhs, rhs) },
onBv = UContext<TSSizeSort>::mkBvAddExpr,
// TODO: support string concatenation here by resolving stringSort.
desiredSort = { _, _ -> fp64Sort },
zishkaz marked this conversation as resolved.
Show resolved Hide resolved
)

object And : TSBinaryOperator(
onBool = UContext<TSSizeSort>::mkAnd,
desiredSort = { _, _ -> boolSort },
)

internal operator fun invoke(lhs: UExpr<out USort>, rhs: UExpr<out USort>): UExpr<out USort> {
internal operator fun invoke(lhs: UExpr<out USort>, rhs: UExpr<out USort>, scope: TSStepScope): UExpr<out USort> {
val bannedSorts = lhs.tctx.banSorts(lhs, rhs)

fun apply(lhs: UExpr<out USort>, rhs: UExpr<out USort>): UExpr<out USort>? {
val ctx = lhs.tctx
val lhsSort = lhs.sort
val rhsSort = rhs.sort
if (lhsSort != rhsSort) error("Sorts must be equal: $lhsSort != $rhsSort")

// banSorts filtering.
if (lhsSort in bannedSorts) return null
// desiredSort filtering.
if (ctx.desiredSort(lhsSort, rhsSort) != lhsSort) return null
zishkaz marked this conversation as resolved.
Show resolved Hide resolved

return when (lhs.sort) {
is UBoolSort -> ctx.onBool(lhs.cast(), rhs.cast())
is UBvSort -> ctx.onBv(lhs.cast(), rhs.cast())
is UFpSort -> ctx.onFp(lhs.cast(), rhs.cast())
is UAddressSort -> ctx.onRef(lhs.cast(), rhs.cast())
else -> error("Unexpected sorts: $lhsSort, $rhsSort")
}
}

val lhsSort = lhs.sort
val rhsSort = rhs.sort

if (lhsSort != rhsSort) TODO("Implement type coercion")
val ctx = lhs.tctx
// Chosen sort is only used to intersect both exprCaches and
// have at least one sort to apply binary operation to.
// All sorts are examined in TSExprTransformer class and not limited by this "chosen one".
val sort = ctx.desiredSort(lhsSort, rhsSort)

return when {
zishkaz marked this conversation as resolved.
Show resolved Hide resolved
lhsSort is UBoolSort -> lhs.tctx.onBool(lhs.cast(), rhs.cast())
lhsSort is UBvSort -> lhs.tctx.onBv(lhs.cast(), rhs.cast())
lhsSort is UFpSort -> lhs.tctx.onFp(lhs.cast(), rhs.cast())

else -> error("Unexpected sorts: $lhsSort, $rhsSort")
lhs is TSWrappedValue -> lhs.coerceWithSort(rhs, ::apply, sort)
else -> TSWrappedValue(ctx, lhs, scope).coerceWithSort(rhs, ::apply, sort)
}
}

Expand Down
2 changes: 2 additions & 0 deletions usvm-ts/src/main/kotlin/org/usvm/TSContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.jacodb.ets.base.EtsBooleanType
import org.jacodb.ets.base.EtsNumberType
import org.jacodb.ets.base.EtsRefType
import org.jacodb.ets.base.EtsType
import org.jacodb.ets.base.EtsUnknownType

typealias TSSizeSort = UBv32Sort

Expand All @@ -17,6 +18,7 @@ class TSContext(components: TSComponents) : UContext<TSSizeSort>(components) {
is EtsBooleanType -> boolSort
is EtsNumberType -> fp64Sort
is EtsRefType -> addressSort
is EtsUnknownType -> addressSort
else -> TODO("Support all JacoDB types")
}

Expand Down
Loading
Loading