From 5444bb1d6202955e14043435a8de81b604f18df3 Mon Sep 17 00:00:00 2001 From: Emil Date: Sun, 26 Nov 2023 22:39:30 +0300 Subject: [PATCH 1/9] Import Go SSA/JacoDB prototype Source-commit: 717bd41613fbc4bb8d8a0af43f3900bab824dcb8 Preserve the original Go module while registering it on current USVM and pinning its Go-specific JacoDB dependency separately. --- buildSrc/src/main/kotlin/Dependencies.kt | 6 + settings.gradle.kts | 1 + .../src/main/kotlin/org/usvm/Composition.kt | 2 + .../main/kotlin/org/usvm/ExprTransformer.kt | 2 + .../src/main/kotlin/org/usvm/Expressions.kt | 25 + .../collection/PrimitiveMapCollectionApi.kt | 127 ++ .../kotlin/org/usvm/solver/ExprTranslator.kt | 6 + .../usvm/solver/USoftConstraintsProvider.kt | 3 + usvm-go/.gitignore | 3 + usvm-go/build.gradle.kts | 29 + usvm-go/src/main/go/examples/array.go | 15 + usvm-go/src/main/go/examples/constraint.go | 13 + usvm-go/src/main/go/examples/defer.go | 81 ++ usvm-go/src/main/go/examples/global.go | 25 + usvm-go/src/main/go/examples/if.go | 73 ++ usvm-go/src/main/go/examples/imports/slice.go | 12 + .../src/main/go/examples/imports/strings.go | 3 + usvm-go/src/main/go/examples/leetcode.go | 70 ++ usvm-go/src/main/go/examples/loop.go | 73 ++ .../src/main/go/examples/make_interface.go | 19 + usvm-go/src/main/go/examples/map.go | 153 +++ usvm-go/src/main/go/examples/object.go | 31 + usvm-go/src/main/go/examples/pointer.go | 28 + usvm-go/src/main/go/examples/slice.go | 197 +++ usvm-go/src/main/go/examples/string.go | 25 + usvm-go/src/main/go/examples/struct.go | 7 + usvm-go/src/main/go/examples/type.go | 144 +++ usvm-go/src/main/go/go.mod | 16 + usvm-go/src/main/go/go.sum | 20 + usvm-go/src/main/go/main.go | 54 + usvm-go/src/main/go/model.go | 494 ++++++++ usvm-go/src/main/go/pack.go | 711 +++++++++++ usvm-go/src/main/go/ssa.go | 129 ++ usvm-go/src/main/go/util.go | 31 + .../kotlin/org/usvm/GoApplicationGraph.kt | 53 + .../src/main/kotlin/org/usvm/GoComponents.kt | 35 + usvm-go/src/main/kotlin/org/usvm/GoContext.kt | 94 ++ usvm-go/src/main/kotlin/org/usvm/GoDomain.kt | 69 ++ .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 1082 +++++++++++++++++ .../src/main/kotlin/org/usvm/GoInstVisitor.kt | 203 ++++ usvm-go/src/main/kotlin/org/usvm/GoMachine.kt | 172 +++ .../main/kotlin/org/usvm/GoMachineOptions.kt | 6 + usvm-go/src/main/kotlin/org/usvm/GoTarget.kt | 8 + .../src/main/kotlin/org/usvm/api/Exception.kt | 10 + .../org/usvm/interpreter/GoInterpreter.kt | 128 ++ .../org/usvm/interpreter/GoTestInterpreter.kt | 452 +++++++ .../main/kotlin/org/usvm/model/Converter.kt | 464 +++++++ .../src/main/kotlin/org/usvm/model/Model.kt | 642 ++++++++++ .../src/main/kotlin/org/usvm/model/Parser.kt | 98 ++ .../org/usvm/operator/GoBinaryOperator.kt | 144 +++ .../org/usvm/operator/GoOperatorUtils.kt | 76 ++ .../org/usvm/operator/GoUnaryOperator.kt | 73 ++ .../kotlin/org/usvm/state/GoMethodResult.kt | 21 + .../src/main/kotlin/org/usvm/state/GoState.kt | 401 ++++++ .../main/kotlin/org/usvm/state/GoStateData.kt | 42 + .../src/main/kotlin/org/usvm/type/GoType.kt | 92 ++ .../main/kotlin/org/usvm/type/GoTypeSystem.kt | 67 + .../main/kotlin/org/usvm/util/Extensions.kt | 28 + .../src/test/kotlin/org/usvm/JacoDbTest.kt | 104 ++ .../test/kotlin/org/usvm/model/ModelTest.kt | 21 + usvm-go/src/test/resources/logback.xml | 12 + .../org/usvm/algorithms/WeightedAaTree.kt | 2 +- 62 files changed, 7226 insertions(+), 1 deletion(-) create mode 100644 usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt create mode 100644 usvm-go/.gitignore create mode 100644 usvm-go/build.gradle.kts create mode 100644 usvm-go/src/main/go/examples/array.go create mode 100644 usvm-go/src/main/go/examples/constraint.go create mode 100644 usvm-go/src/main/go/examples/defer.go create mode 100644 usvm-go/src/main/go/examples/global.go create mode 100644 usvm-go/src/main/go/examples/if.go create mode 100644 usvm-go/src/main/go/examples/imports/slice.go create mode 100644 usvm-go/src/main/go/examples/imports/strings.go create mode 100644 usvm-go/src/main/go/examples/leetcode.go create mode 100644 usvm-go/src/main/go/examples/loop.go create mode 100644 usvm-go/src/main/go/examples/make_interface.go create mode 100644 usvm-go/src/main/go/examples/map.go create mode 100644 usvm-go/src/main/go/examples/object.go create mode 100644 usvm-go/src/main/go/examples/pointer.go create mode 100644 usvm-go/src/main/go/examples/slice.go create mode 100644 usvm-go/src/main/go/examples/string.go create mode 100644 usvm-go/src/main/go/examples/struct.go create mode 100644 usvm-go/src/main/go/examples/type.go create mode 100644 usvm-go/src/main/go/go.mod create mode 100644 usvm-go/src/main/go/go.sum create mode 100644 usvm-go/src/main/go/main.go create mode 100644 usvm-go/src/main/go/model.go create mode 100644 usvm-go/src/main/go/pack.go create mode 100644 usvm-go/src/main/go/ssa.go create mode 100644 usvm-go/src/main/go/util.go create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoComponents.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoContext.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoDomain.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoMachine.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/GoTarget.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/api/Exception.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/model/Converter.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/model/Model.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/model/Parser.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/state/GoState.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/type/GoType.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt create mode 100644 usvm-go/src/test/resources/logback.xml diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index c3fd4b5fd6..753c5d7955 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -117,6 +117,12 @@ object Libs { // https://github.com/UnitTestBot/jacodb private const val jacodbPackage = "com.github.UnitTestBot.jacodb" // use "org.jacodb" with includeBuild + val jacodb_go = dep( + group = jacodbPackage, + name = "jacodb-go", + version = "816194b963" + ) + val jacodb_core = dep( group = jacodbPackage, name = "jacodb-core", diff --git a/settings.gradle.kts b/settings.gradle.kts index 4331282cae..a9051ba765 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ develocity { } include("usvm-core") +include("usvm-go") include("usvm-detekt-rules") include("usvm-jvm") include("usvm-jvm:usvm-jvm-api") diff --git a/usvm-core/src/main/kotlin/org/usvm/Composition.kt b/usvm-core/src/main/kotlin/org/usvm/Composition.kt index 7f23afc945..f8863982fb 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Composition.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Composition.kt @@ -122,6 +122,8 @@ open class UComposer( override fun transform(expr: UConcreteHeapRef): UExpr = expr override fun transform(expr: UNullRef): UExpr = memory.nullRef() + + override fun transform(expr: ULValuePointer): UExpr = expr } @Suppress("NOTHING_TO_INLINE") diff --git a/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt b/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt index 57e2e2640c..3a6b173cb7 100644 --- a/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt +++ b/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt @@ -67,6 +67,8 @@ interface UTransformer : KTransformer { fun transform(expr: UConcreteHeapRef): UExpr fun transform(expr: UNullRef): UExpr + + fun transform(expr: ULValuePointer): UExpr } abstract class UExprTransformer( diff --git a/usvm-core/src/main/kotlin/org/usvm/Expressions.kt b/usvm-core/src/main/kotlin/org/usvm/Expressions.kt index 927c8168ba..3139c3f5c8 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Expressions.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Expressions.kt @@ -21,6 +21,7 @@ import io.ksmt.sort.KBvSort import io.ksmt.sort.KFpSort import io.ksmt.sort.KSort import io.ksmt.sort.KUninterpretedSort +import org.usvm.memory.ULValue import org.usvm.memory.USymbolicCollection import org.usvm.memory.USymbolicCollectionId import kotlin.contracts.ExperimentalContracts @@ -315,6 +316,30 @@ class UIsSupertypeExpr internal constructor( //endregion +//region Pointer Semantics + +class ULValuePointer( + ctx: UContext<*>, + var lvalue: ULValue<*, *> +) : UExpr(ctx) { + override val sort: UAddressSort = ctx.addressSort + + override fun accept(transformer: KTransformerBase): KExpr { + require(transformer is UTransformer<*, *>) { "Expected a UTransformer, but got: $transformer" } + return transformer.transform(this) + } + + override fun internEquals(other: Any): Boolean = structurallyEqual(other) { lvalue } + + override fun internHashCode(): Int = hash(lvalue) + + override fun print(printer: ExpressionPrinter) { + printer.append("&$lvalue") + } +} + +//endregion + //region Utils val UBoolExpr.isFalse get() = this == ctx.falseExpr diff --git a/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt b/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt new file mode 100644 index 0000000000..44163af064 --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt @@ -0,0 +1,127 @@ +package org.usvm.api.collection + +import io.ksmt.utils.uncheckedCast +import org.usvm.UBoolExpr +import org.usvm.UContext +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.USort +import org.usvm.UState +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapSize +import org.usvm.api.makeSymbolicPrimitive +import org.usvm.api.setContainsElement +import org.usvm.collection.map.length.UMapLengthLValue +import org.usvm.collection.map.primitive.UMapEntryLValue +import org.usvm.collection.map.primitive.mapMerge +import org.usvm.collection.set.primitive.USetEntryLValue +import org.usvm.collection.set.primitive.USetRegionId +import org.usvm.collection.set.primitive.setEntries +import org.usvm.collection.set.primitive.setUnion +import org.usvm.isFalse +import org.usvm.isTrue +import org.usvm.memory.USymbolicCollectionKeyInfo +import org.usvm.mkSizeAddExpr +import org.usvm.mkSizeExpr +import org.usvm.mkSizeSubExpr +import org.usvm.regions.Region +import org.usvm.sizeSort + +object PrimitiveMapCollectionApi { + fun > UState.symbolicPrimitiveMapGet( + mapRef: UHeapRef, + key: UExpr, + mapType: MapType, + valueSort: ValueSort, + keyInfo: USymbolicCollectionKeyInfo, Reg>, + ): UExpr = memory.read(UMapEntryLValue(key.sort, valueSort, mapRef, key, mapType, keyInfo)) + + fun > UState.symbolicPrimitiveMapContains( + mapRef: UHeapRef, + key: UExpr, + mapType: MapType, + keyInfo: USymbolicCollectionKeyInfo, Reg>, + ): UBoolExpr = memory.setContainsElement(mapRef, key, mapType, keyInfo) + + fun > UState.symbolicPrimitiveMapAnyKey( + mapRef: UHeapRef, + mapType: MapType, + keySort: KeySort, + keyInfo: USymbolicCollectionKeyInfo, Reg>, + ): UExpr { + val allKeys = memory.setEntries(mapRef, mapType, keySort, keyInfo) + val symbolicKeys = mutableListOf, UBoolExpr>>() + for (entry in allKeys.entries) { + val key = entry.setElement + val contains = symbolicPrimitiveMapContains(mapRef, key, mapType, keyInfo) + when { + contains.isTrue -> return key + contains.isFalse -> continue + else -> symbolicKeys += key to contains + } + } + + val defaultKey = makeSymbolicPrimitive(keySort) + return symbolicKeys.fold(defaultKey) { result, (key, contains) -> + ctx.mkIte(contains, key, result) + } + } + + fun , KeySort : USort, ValueSort : USort, Reg : Region> UState.symbolicPrimitiveMapPut( + mapRef: UHeapRef, + key: UExpr, + value: UExpr, + mapType: MapType, + keyInfo: USymbolicCollectionKeyInfo, Reg>, + ) = with(ctx) { + val mapContainsLValue = USetEntryLValue(key.sort, mapRef, key, mapType, keyInfo) + val currentSize = symbolicObjectMapSize(mapRef, mapType) + + val keyIsInMap = memory.read(mapContainsLValue) + val keyIsNew = mkNot(keyIsInMap) + + memory.write(UMapEntryLValue(key.sort, value.sort, mapRef, key, mapType, keyInfo), value, guard = trueExpr) + memory.write(mapContainsLValue, rvalue = trueExpr, guard = trueExpr) + + val updatedSize = mkSizeAddExpr(currentSize, mkSizeExpr(1)) + memory.write(UMapLengthLValue(mapRef, mapType, sizeSort), updatedSize, keyIsNew) + } + + fun , KeySort : USort, Reg : Region> UState.symbolicPrimitiveMapRemove( + mapRef: UHeapRef, + key: UExpr, + mapType: MapType, + keyInfo: USymbolicCollectionKeyInfo, Reg>, + ) = with(ctx) { + val mapContainsLValue = USetEntryLValue(key.sort, mapRef, key, mapType, keyInfo) + val currentSize = symbolicObjectMapSize(mapRef, mapType) + + val keyIsInMap = memory.read(mapContainsLValue) + + // todo: skip values update? + memory.write(mapContainsLValue, rvalue = falseExpr, guard = trueExpr) + + val updatedSize = mkSizeSubExpr(currentSize, mkSizeExpr(1)) + memory.write(UMapLengthLValue(mapRef, mapType, sizeSort), updatedSize, keyIsInMap) + } + + fun , KeySort : USort, ValueSort : USort, Reg : Region> UState.symbolicPrimitiveMapMergeInto( + dstRef: UHeapRef, + srcRef: UHeapRef, + mapType: MapType, + keySort: KeySort, + valueSort: ValueSort, + keyInfo: USymbolicCollectionKeyInfo, Reg>, + ) = with(ctx) { + val srcMapSize = symbolicObjectMapSize(srcRef, mapType) + val dstMapSize = symbolicObjectMapSize(dstRef, mapType) + + val containsSetId = USetRegionId(keySort, mapType, keyInfo) + memory.mapMerge(srcRef, dstRef, mapType, keySort, valueSort, keyInfo, containsSetId.uncheckedCast(), guard = trueExpr) + memory.setUnion(srcRef, dstRef, mapType, keySort, keyInfo, guard = trueExpr) + + // todo: precise map size approximation? + // val sizeLowerBound = mkIte(mkBvSignedGreaterExpr(srcMapSize, dstMapSize), srcMapSize, dstMapSize) + val sizeUpperBound = mkSizeAddExpr(srcMapSize, dstMapSize) + memory.write(UMapLengthLValue(dstRef, mapType, sizeSort), sizeUpperBound, guard = trueExpr) + } +} diff --git a/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt b/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt index 45f4499fa7..18722e300c 100644 --- a/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt +++ b/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt @@ -17,6 +17,7 @@ import org.usvm.UIndexedMethodReturnValue import org.usvm.UIsExpr import org.usvm.UIsSubtypeExpr import org.usvm.UIsSupertypeExpr +import org.usvm.ULValuePointer import org.usvm.UNullRef import org.usvm.URegisterReading import org.usvm.USort @@ -94,6 +95,11 @@ open class UExprTranslator( return const } + override fun transform(expr: ULValuePointer): KExpr { + val const = expr.sort.mkConst("&${expr.lvalue}") + return const + } + override fun transform(expr: UConcreteHeapRef): KExpr { require(isStaticHeapRef(expr)) { "Unexpected ref: $expr" } diff --git a/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt b/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt index 64c5820f73..865fa7e12a 100644 --- a/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt +++ b/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt @@ -31,6 +31,7 @@ import org.usvm.UExpr import org.usvm.UIndexedMethodReturnValue import org.usvm.UIsSubtypeExpr import org.usvm.UIsSupertypeExpr +import org.usvm.ULValuePointer import org.usvm.UNullRef import org.usvm.URegisterReading import org.usvm.USort @@ -124,6 +125,8 @@ open class USoftConstraintsProvider( override fun transform(expr: UNullRef): UExpr = expr + override fun transform(expr: ULValuePointer): UExpr = expr + override fun transform(expr: UIsSubtypeExpr): UBoolExpr = expr override fun transform(expr: UIsSupertypeExpr): UBoolExpr = expr diff --git a/usvm-go/.gitignore b/usvm-go/.gitignore new file mode 100644 index 0000000000..31a5fd3547 --- /dev/null +++ b/usvm-go/.gitignore @@ -0,0 +1,3 @@ +src/**/gen +src/main/go/dump/ +out/ diff --git a/usvm-go/build.gradle.kts b/usvm-go/build.gradle.kts new file mode 100644 index 0000000000..c5d3fd61c7 --- /dev/null +++ b/usvm-go/build.gradle.kts @@ -0,0 +1,29 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") + id("org.gradle.application") + id("usvm.kotlin-conventions") + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" +} + +tasks { + withType { + kotlinOptions { + allWarningsAsErrors = false + } + } +} + +dependencies { + implementation(project(":usvm-core")) + + implementation(Libs.jacodb_go) + implementation(Libs.kotlinx_serialization_core) + implementation(Libs.kotlinx_serialization_json) + implementation(Libs.kotlinx_collections) + implementation(Libs.ksmt_yices) + implementation(Libs.slf4j_simple) + + testImplementation(Libs.logback) +} \ No newline at end of file diff --git a/usvm-go/src/main/go/examples/array.go b/usvm-go/src/main/go/examples/array.go new file mode 100644 index 0000000000..2b9bb66077 --- /dev/null +++ b/usvm-go/src/main/go/examples/array.go @@ -0,0 +1,15 @@ +package examples + +func arrayIndex(arr [3]int, i int) int { + return arr[i] +} + +func arrayIndexMake(i int) int { + arr := [3]int{1, 2, 3} + return arr[i] +} + +func arraySlice() []int { + arr := [3]int{1, 2, 3} + return arr[1:2] +} diff --git a/usvm-go/src/main/go/examples/constraint.go b/usvm-go/src/main/go/examples/constraint.go new file mode 100644 index 0000000000..1dcd8cd19c --- /dev/null +++ b/usvm-go/src/main/go/examples/constraint.go @@ -0,0 +1,13 @@ +package examples + +func checkGoodString(s string) { + if len(s) < 3 { + panic("string too small") + } + + for _, r := range s { + if r < 97 || r > 122 { + panic("bad char") + } + } +} diff --git a/usvm-go/src/main/go/examples/defer.go b/usvm-go/src/main/go/examples/defer.go new file mode 100644 index 0000000000..68bbdc99c2 --- /dev/null +++ b/usvm-go/src/main/go/examples/defer.go @@ -0,0 +1,81 @@ +package examples + +func simple(a int) (b int) { + defer func() { + b++ + defer func() { + b++ + recover() + }() + defer func() { + defer func() { + defer func() { + b++ + if b > 3 { + panic("fail") + } + }() + b++ + }() + b++ + }() + }() + + return a +} + +func verySimple(a int) (b int) { + if a == 3 { + return 3 + } + defer func() { + b = 5 + }() + return a +} + +func panicking() { + panic("oh no") +} + +func panicRecoverComplex(a int) (b int) { + defer func() { + if r := recover(); r != nil { + b = 3 + } + }() + defer func() { + b = -227 + }() + defer func() { + b = 2 + if r := recover(); r != nil { + b += 17 + } + }() + defer func() { + b = 5 + }() + + panicking() + b = a + 1000 + return b +} + +func panicRecoverSimple() int { + defer func() { + recover() + }() + + panic("fail") +} + +func panicRecoverResultSimple() (a int) { + defer func() { + a += 3 + recover() + a += 5 + }() + + panic("fail") +} diff --git a/usvm-go/src/main/go/examples/global.go b/usvm-go/src/main/go/examples/global.go new file mode 100644 index 0000000000..8889cfd497 --- /dev/null +++ b/usvm-go/src/main/go/examples/global.go @@ -0,0 +1,25 @@ +package examples + +const globalIntConst = 555 + +var globalIntVar = 444 + +func globalSimple(a int) int { + if a > globalIntConst { + globalIntVar = a - globalIntConst + } else if a < globalIntConst { + globalIntVar = globalIntConst - a + } + return globalIntVar +} + +var globalArray [256]int + +func globalArraySimple(i int) int { + if i > 255 || i < 0 { + return -1 + } + + globalArray[i] = 5 + return globalArray[i] +} diff --git a/usvm-go/src/main/go/examples/if.go b/usvm-go/src/main/go/examples/if.go new file mode 100644 index 0000000000..a4b7177838 --- /dev/null +++ b/usvm-go/src/main/go/examples/if.go @@ -0,0 +1,73 @@ +package examples + +func max2(a, b int) int { + if a > b { + return a + } + return b +} + +func max2Anon(a, b int) int { + mx := func(x, y int) int { + if x > y { + return x + } + return y + } + return mx(a, b) +} + +func max2Closure(a, b int) int { + mx := func() int { + if a > b { + return a + } + return b + } + return mx() +} + +func max3(a, b, c int) int { + if a > b && a > c { + return a + } + if b > c { + return b + } + return c +} + +func max3Call(a, b, c int) int { + return max2(max2(a, b), c) +} + +func max4Call(a, b, c, d int) int { + return max2(max2(a, b), max2(c, d)) +} + +func MinPublic(a, b int) int { + if a < b { + return a + } + return b +} + +func inc(a int, f bool) int { + if f { + a++ + } + return a +} + +func beforeAndAfter(a int) (int, int) { + return a - 1, a + 1 +} + +func sumBeforeAndAfter(a int) int { + b, c := beforeAndAfter(a) + return b + c +} + +func call(f func() int) int { + return f() +} diff --git a/usvm-go/src/main/go/examples/imports/slice.go b/usvm-go/src/main/go/examples/imports/slice.go new file mode 100644 index 0000000000..0cd5e09ba9 --- /dev/null +++ b/usvm-go/src/main/go/examples/imports/slice.go @@ -0,0 +1,12 @@ +package imports + +import ( + "sort" +) + +func sliceCast(arr []int) []int { + s := sort.IntSlice(arr) + x := s[0] + s[0] = x + 2 + return []int(s) +} diff --git a/usvm-go/src/main/go/examples/imports/strings.go b/usvm-go/src/main/go/examples/imports/strings.go new file mode 100644 index 0000000000..027c8879dc --- /dev/null +++ b/usvm-go/src/main/go/examples/imports/strings.go @@ -0,0 +1,3 @@ +package imports + +import _ "strings" diff --git a/usvm-go/src/main/go/examples/leetcode.go b/usvm-go/src/main/go/examples/leetcode.go new file mode 100644 index 0000000000..9f8b698ac9 --- /dev/null +++ b/usvm-go/src/main/go/examples/leetcode.go @@ -0,0 +1,70 @@ +package examples + +func twoSum(nums []int, target int) []int { + m := make(map[int]int, len(nums)) + for i := range nums { + n := nums[i] + m[target-n] = i + } + for i := range nums { + n := nums[i] + if _, ok := m[n]; !ok { + continue + } + if m[n] != i { + return []int{i, m[n]} + } + } + + return nil +} + +func containsNearbyDuplicate(nums []int, k int) bool { + m := make(map[int]int, k) + for i := 0; i < len(nums); i++ { + m[nums[i]]++ + if m[nums[i]] >= 2 { + return true + } + if i-k >= 0 { + m[nums[i-k]]-- + } + } + return false +} + +func canVisitAllRooms(rooms [][]int) bool { + n := len(rooms) + if n <= 1 { + panic("too small rooms list") + } + for i := range rooms { + if len(rooms[i]) == 0 { + panic("empty room") + } + for j := range rooms[i] { + if rooms[i][j] < 0 || rooms[i][j] >= n { + panic("illegal key") + } + } + } + + cur := make([]int, 0, n) + cur = append(cur, 0) + next := make([]int, 0, n) + v := make(map[int]bool, n) + for len(cur) > 0 { + for _, i := range cur { + if v[i] { + continue + } + v[i] = true + for _, k := range rooms[i] { + next = append(next, k) + } + } + cur = next + next = make([]int, 0, n) + } + return len(v) == n +} diff --git a/usvm-go/src/main/go/examples/loop.go b/usvm-go/src/main/go/examples/loop.go new file mode 100644 index 0000000000..d23309213f --- /dev/null +++ b/usvm-go/src/main/go/examples/loop.go @@ -0,0 +1,73 @@ +package examples + +func loopSimple(i int) int { + idx := i + for idx < 10 { + idx++ + } + return idx +} + +func loopIf(i int) int { + idx := i + for idx < 10 { + if idx < 5 { + idx++ + } else { + idx += 2 + } + } + return idx +} + +func loopInfinite(i int) int { + for { + j := i + 3 + k := j + i + l := k + j + 4 + j = l + 2 + } +} + +func loopInner(i, j int) int { + sum := 0 + for k := 0; k < 100; k++ { + for l := 0; l < 100; l++ { + if k+l < i+j { + sum += k + l + } + } + } + return sum +} + +func loopCollatz(i int) int { + if i <= 0 || i >= 100 { + return 0 + } + + j := i + loopCount := 0 + + for j != 1 { + oldJ := j + j = j*3 + 1 + if oldJ%2 == 0 { + j = oldJ / 2 + } + loopCount++ + } + + if loopCount == 17 { + return 1 + } + return 2 +} + +func loopSum(n int) int { + res := 0 + for i := 1; i <= n; i++ { + res += i + } + return res +} diff --git a/usvm-go/src/main/go/examples/make_interface.go b/usvm-go/src/main/go/examples/make_interface.go new file mode 100644 index 0000000000..884bb40152 --- /dev/null +++ b/usvm-go/src/main/go/examples/make_interface.go @@ -0,0 +1,19 @@ +package examples + +var shiftError = error(errorString("negative shift amount")) + +type errorString string + +func (e errorString) Error() string { + return "runtime error: " + string(e) +} + +func shiftErrorToString() string { + return shiftError.Error() +} + +func appendErrorStrings() errorString { + s1 := errorString("hello, ") + s2 := errorString("world!") + return s1 + s2 +} diff --git a/usvm-go/src/main/go/examples/map.go b/usvm-go/src/main/go/examples/map.go new file mode 100644 index 0000000000..2f70830a44 --- /dev/null +++ b/usvm-go/src/main/go/examples/map.go @@ -0,0 +1,153 @@ +package examples + +func mapAlloc(l int) map[int]int64 { + m := make(map[int]int64, l) + m[3] = 111 + m[-226] = 13 + m[0] = -1 + return m +} + +func mapLookup(m map[int]int, k int) int { + v := m[k] + if v == 123 { + return -1 + } + return v +} + +func mapLookupComma(m map[int]int, k int) int { + v, ok := m[k] + if !ok { + return 0 + } + if v == 123 { + return -1 + } + return v +} + +func mapLookupCommaReturn(m map[int]int, k int) (int, bool) { + v, ok := m[k] + if ok { + return v, true + } + return -1, false +} + +func mapUpdate(m map[int]int, k, v int) int { + vOld := m[k] + m[k] = v + if v > vOld { + return v + } + return vOld +} + +func mapLoop(m map[int]int, n int) int { + mx := 0 + for k, v := range m { + if k > n && v > mx { + mx = v + } + } + + return mx +} + +func mapLoopLen(m map[int]int) int { + if len(m) < 4 { + return -1 + } + + minKey, minValue := 0, 0 + maxKey, maxValue := 0, 0 + for k, v := range m { + if v > maxValue { + maxKey = k + maxValue = v + } + if v < minValue { + minKey = k + minValue = v + } + } + if minValue == maxValue { + return minValue + } + + return m[maxKey] - m[minKey] +} + +func mapDeleteSimple(a map[int]int, k int) int { + l := len(a) + + if l < 5 { + panic("too smol map") + } + + delete(a, k) + if len(a) == l { + panic("not found") + } + + return len(a) +} + +type customMap map[int]int + +func mapCustomAlloc() customMap { + m := make(customMap) + m[2] = 3 + return m +} +func mapCustomLookup(m customMap, k int) int { + v := m[k] + if v == 123 { + return -1 + } + return v +} + +func mapCustomLookupComma(m customMap, k int) int { + v, ok := m[k] + if !ok { + return 0 + } + if v == 123 { + return -1 + } + return v +} + +func mapCustomLookupCommaReturn(m customMap, k int) (int, bool) { + v, ok := m[k] + if ok { + return v, true + } + return -1, false +} + +func mapCustomUpdate(m customMap, k, v int) int { + vOld := m[k] + m[k] = v + if v > vOld { + return v + } + return vOld +} + +func mapCustomDeleteSimple(a customMap, k int) int { + l := len(a) + + if l < 5 { + panic("too smol map") + } + + delete(a, k) + if len(a) == l { + panic("not found") + } + + return len(a) +} diff --git a/usvm-go/src/main/go/examples/object.go b/usvm-go/src/main/go/examples/object.go new file mode 100644 index 0000000000..27a2bc84d2 --- /dev/null +++ b/usvm-go/src/main/go/examples/object.go @@ -0,0 +1,31 @@ +package examples + +type Object struct { + value int +} + +func (o *Object) Get() int { + if o.value == 0 { + return -1 + } + return o.value +} + +func (o *Object) Set(i int) { + o.value = i +} + +func (o *Object) SetAndReturn(i int) int { + if o == nil { + return -1001 + } + if i == o.value { + return 1001 + } + o.value = i + return o.value +} + +func ModifyAndGet(o *Object, i int) int { + return o.SetAndReturn(i) +} diff --git a/usvm-go/src/main/go/examples/pointer.go b/usvm-go/src/main/go/examples/pointer.go new file mode 100644 index 0000000000..2dfd944e3c --- /dev/null +++ b/usvm-go/src/main/go/examples/pointer.go @@ -0,0 +1,28 @@ +package examples + +func pointerSimple(i int) int { + j := &i + k := &j + l := &k + **k += 8 + **l = *k + return ***l + 1 +} + +func pointerOther(i int) int { + j := &i + *j += 5 + k := &j + return **k + 1 +} + +func pointerAnother(i int) int { + a := 5 + b := &a + *b = 3 + return a +} + +func pointerChangeType(i *int) *NamedInt { + return (*NamedInt)(i) +} diff --git a/usvm-go/src/main/go/examples/slice.go b/usvm-go/src/main/go/examples/slice.go new file mode 100644 index 0000000000..d2603b482d --- /dev/null +++ b/usvm-go/src/main/go/examples/slice.go @@ -0,0 +1,197 @@ +package examples + +func sliceSimple(arr []int) int { + arr[0] = 5 + return arr[0] +} + +func sliceOverwrite(arr []int) int { + if len(arr) == 0 { + return -1 + } + arr[0] = 152 + return arr[0] +} + +func sliceAlloc(l int) []int { + arr := make([]int, l) + if l < 5 || l > 10 { + return arr + } + arr[3] = 111 + return arr +} + +func sliceFirst(nums []int) int { + if len(nums) < 2 || len(nums) > 20 { + return -1 + } + if nums[0] == 1 && nums[1] == 2 { + return 1 + } + return 0 +} + +func sliceSum(nums []int) int { + if len(nums) < 5 { + return -228 + } + res := 0 + for i := range nums { + if nums[i] > 0 { + res += nums[i] + } else if nums[i] < 0 { + res -= nums[i] + } + } + return res +} + +func sliceCompare(nums []int) int { + i := nums[0] + j := nums[1] + if i < j { + return i + } + return j +} + +var sliceComparator = sliceCompare + +func sliceCompareFuncVar(nums []int) int { + if sliceComparator(nums) == 5 { + return 3 + } + return 1 +} + +func sliceSliceFull(nums []int) []int { + if len(nums) < 5 || len(nums) > 10 { + return nil + } + return nums[:] +} + +func sliceSliceFrom(nums []int, i int) []int { + if len(nums) < 5 || len(nums) > 10 { + return nil + } + return nums[i:] +} + +func sliceSliceTo(nums []int, i int) []int { + if len(nums) < 5 || len(nums) > 10 || i > 3 { + return nil + } + return nums[:i] +} + +func sliceSlice(nums []int, i, j int) []int { + if len(nums) < 5 || len(nums) > 10 || i == j { + return nil + } + + n := nums[:] + n = n[:j] + n = n[i:] + return n +} + +func sliceToArrayPointer(nums []int) *[3]int { + a := (*[3]int)(nums) + a[1] = 1 + return a +} + +func sliceAppend(nums []int) []int { + nums = append(nums, 3) + nums = append(nums, []int{1, 2}...) + return append(nums, 4) +} + +func sliceAppendTwo(nums1, nums2 []int) []int { + if len(nums1) < 5 || len(nums1) > 10 || len(nums2) < 5 || len(nums2) > 10 { + return nil + } + + nums := append(nums1, nums2...) + return append(nums, 4) +} + +func sliceAppendSimple(a []int) []int { + if len(a) == 0 || a == nil { + return []int{1, 2} + } + + var c []int = nil + b := append(c, 5, 6) + return append(a, b...) +} + +func sliceSumMatrix(matrix [][]int) int { + if len(matrix) < 3 { + panic("too small") + } + if len(matrix[0]) < 3 { + panic("too small") + } + + s := 0 + for _, m := range matrix { + for _, n := range m { + s += n + } + } + return s +} + +func sliceCopySimple(a []int) []int { + if len(a) == 0 { + return a + } + + a[0] = 5 + b := make([]int, len(a)) + copy(b, a) + return b +} + +type customSlice []int + +func sliceCustomOverwrite(arr customSlice) int { + if len(arr) == 0 { + return -1 + } + arr[0] = 152 + return arr[0] +} + +func sliceCustomAppend() customSlice { + c := make(customSlice, 0) + c = append(c, customSlice{1, 2, 3}...) + c = append(c, []int{4, 5}...) + c = append(c, 123) + return c +} + +func sliceCustomSlice() customSlice { + c := customSlice{1, 2, 3, 4, 5} + return c[1:3] +} + +func sliceCustomCopySimple(a customSlice) customSlice { + if len(a) == 0 { + return a + } + + a[0] = 5 + b := make(customSlice, len(a)) + copy(b, a) + return b +} + +func sliceCustomToArrayPointer(nums customSlice) *[3]int { + a := (*[3]int)(nums) + a[1] = 1 + return a +} diff --git a/usvm-go/src/main/go/examples/string.go b/usvm-go/src/main/go/examples/string.go new file mode 100644 index 0000000000..456df009b2 --- /dev/null +++ b/usvm-go/src/main/go/examples/string.go @@ -0,0 +1,25 @@ +package examples + +func stringGetByte(s string, i int) byte { + checkGoodString(s) + return s[i] +} + +func stringGetRune(s string, i int) rune { + checkGoodString(s) + return rune(s[i]) +} + +func stringAppend() string { + s1 := "hello, " + s2 := "world" + return s1 + s2 +} + +func stringToByteArray() []byte { + return []byte("hello") +} + +func stringFromByteArray() string { + return string([]byte{104, 101, 108, 108, 111}) +} diff --git a/usvm-go/src/main/go/examples/struct.go b/usvm-go/src/main/go/examples/struct.go new file mode 100644 index 0000000000..d91573e8de --- /dev/null +++ b/usvm-go/src/main/go/examples/struct.go @@ -0,0 +1,7 @@ +package examples + +func nameSmall(p Person) bool { + checkGoodString(p.Name) + + return len(p.Name) < 5 +} diff --git a/usvm-go/src/main/go/examples/type.go b/usvm-go/src/main/go/examples/type.go new file mode 100644 index 0000000000..8dd523fc70 --- /dev/null +++ b/usvm-go/src/main/go/examples/type.go @@ -0,0 +1,144 @@ +package examples + +type Person struct { + Name string + Age int +} + +func (p Person) GetName() string { + return p.Name +} + +func (p Person) GetAge() int { + return p.Age +} + +func (p Person) WithName(name string) Creature { + checkGoodString(name) + p.Name = name + return p +} + +func (p Person) Validate() (bool, error) { + return true, nil +} + +type Creature interface { + GetAge() int + GetName() string + WithName(n string) Creature + Validate() (bool, error) +} + +func GetAge(p Person) int { + return p.GetAge() +} + +type Building struct { + Height int +} + +type NamedInt int + +func (n NamedInt) square() int { + if n == 0 { + panic("zero") + } + if n == 1 || n == -1 { + panic("one") + } + + return int(n * n) +} + +func toNamedInt(i int) NamedInt { + return NamedInt(i + 1) +} + +func callNamedInt(i int) int { + return NamedInt(i).square() +} + +func callCreature(c Creature) int { + if c == nil { + return -1 + } + + return c.GetAge() +} + +func assertCreature() (Person, bool) { + var c Creature = Person{ + Name: "Name", + Age: 42, + } + p, ok := c.(Person) + return p, ok +} + +func assertCreatureNoComma() Person { + var c Creature = Person{ + Name: "Name", + Age: 42, + } + return c.(Person) +} + +func assertCreatureFailNoComma() Building { + var c any = Person{ + Name: "Name", + Age: 42, + } + return c.(Building) +} + +func assertCreaturePointer() (*Person, bool) { + var c Creature = &Person{ + Name: "Name", + Age: 42, + } + p, ok := c.(*Person) + return p, ok +} + +func assertCreatureArgument(c Creature) (Person, bool) { + if p, ok := c.(Person); ok { + return p, ok + } + + return Person{}, false +} + +func assertCreatureArgumentCall(c Creature) int { + if p, ok := c.(Person); ok { + return p.GetAge() + } + + return -1 +} + +func assertNamedIntCall(i any) int { + if n, ok := i.(NamedInt); ok { + return n.square() + } + if n, ok := i.(int); ok { + if n*n <= 1 { + panic("invalid int") + } + return n * n + } + return -1 +} + +func assertIntAny() int { + var a any = 2 + var b any = 3 + return a.(int) + b.(int) +} + +type Node struct { + left *Node + right *Node +} + +type X *X diff --git a/usvm-go/src/main/go/go.mod b/usvm-go/src/main/go/go.mod new file mode 100644 index 0000000000..a932320e72 --- /dev/null +++ b/usvm-go/src/main/go/go.mod @@ -0,0 +1,16 @@ +module usvm + +go 1.22.3 + +require ( + github.com/samber/lo v1.47.0 + golang.org/x/tools v0.24.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/usvm-go/src/main/go/go.sum b/usvm-go/src/main/go/go.sum new file mode 100644 index 0000000000..ea027a647a --- /dev/null +++ b/usvm-go/src/main/go/go.sum @@ -0,0 +1,20 @@ +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/usvm-go/src/main/go/main.go b/usvm-go/src/main/go/main.go new file mode 100644 index 0000000000..c6491b6498 --- /dev/null +++ b/usvm-go/src/main/go/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "flag" + "log" + "os" + "runtime/pprof" + "time" +) + +var ( + packageName = flag.String("packageName", "usvm/examples", "Full package name") + enableTracing = flag.Bool("enable-tracing", false, "Enables tracing via Logf") + + dumpSSA = flag.Bool("dump-ssa", true, "Dumps SSA") + dumpSSAFile = flag.String("dump-ssa-file", "dump/ssadump.txt", "SSA output file") + + dryRun = flag.Bool("dry-run", false, "Dry run") + profile = flag.Bool("profile", false, "Run profiling") +) + +func main() { + now := time.Now() + flag.Parse() + + if *profile { + f, err := os.Create("dump/profile.out") + CheckError(err) + CheckError(pprof.StartCPUProfile(f)) + defer func() { + pprof.StopCPUProfile() + CheckError(f.Close()) + }() + } + + if *packageName == "" { + log.Fatal("Fatal: missing package name") + } + + config := Config{ + EnableTracing: *enableTracing, + DumpSSA: *dumpSSA, + DumpSSAFileName: *dumpSSAFile, + } + + ssaInfo, err := NewSSA(*packageName, config) + CheckError(err) + + if !*dryRun { + ssaInfo.Write() + } + + log.Printf("Done! Took %.3f seconds\n", time.Since(now).Seconds()) +} diff --git a/usvm-go/src/main/go/model.go b/usvm-go/src/main/go/model.go new file mode 100644 index 0000000000..c9cd53da4d --- /dev/null +++ b/usvm-go/src/main/go/model.go @@ -0,0 +1,494 @@ +package main + +import ( + "golang.org/x/tools/go/ssa" +) + +const ( + NamedConstMember = "NamedConst" + GlobalMember = "Global" + FunctionMember = "Function" + TypeMember = "Type" + + DebugRefInstruction = "DebugRef" + UnOpInstruction = "UnOp" + BinOpInstruction = "BinOp" + CallInstruction = "Call" + ChangeInterfaceInstruction = "ChangeInterface" + ChangeTypeInstruction = "ChangeType" + ConvertInstruction = "Convert" + SliceToArrayPointerInstruction = "SliceToArrayPointer" + MakeInterfaceInstruction = "MakeInterface" + ExtractInstruction = "Extract" + SliceInstruction = "Slice" + ReturnInstruction = "Return" + RunDefersInstruction = "RunDefers" + PanicInstruction = "Panic" + SendInstruction = "Send" + StoreInstruction = "Store" + IfInstruction = "If" + JumpInstruction = "Jump" + DeferInstruction = "Defer" + GoInstruction = "Go" + MakeChanInstruction = "MakeChan" + AllocInstruction = "Alloc" + MakeSliceInstruction = "MakeSlice" + MakeMapInstruction = "MakeMap" + RangeInstruction = "Range" + NextInstruction = "Next" + FieldAddrInstruction = "FieldAddr" + FieldInstruction = "Field" + IndexAddrInstruction = "IndexAddr" + IndexInstruction = "Index" + LookupInstruction = "Lookup" + MapUpdateInstruction = "MapUpdate" + TypeAssertInstruction = "TypeAssert" + MakeClosureInstruction = "MakeClosure" + PhiInstruction = "Phi" + SelectInstruction = "Select" + MultiConvertInstruction = "MultiConvert" + + ConstValue = "Const" + GlobalValue = "Global" + ParameterValue = "Parameter" + FreeVarValue = "FreeVar" + VarValue = "Var" + + FunctionValue = "Function" + MakeClosureValue = "MakeClosure" + BuiltinValue = "Builtin" + + AliasType = "Alias" + ArrayType = "Array" + BasicType = "Basic" + ChanType = "Chan" + InterfaceType = "Interface" + MapType = "Map" + NamedType = "Named" + OpaqueType = "Opaque" + PointerType = "Pointer" + SignatureType = "Signature" + SliceType = "Slice" + StructType = "Struct" + TupleType = "Tuple" + TypeParamType = "TypeParam" + UnionType = "Union" +) + +type Package struct { + Name string `yaml:"name" json:"name"` + Members []Member `yaml:"members" json:"members"` + Types map[string]Type `yaml:"types" json:"types"` + + program *ssa.Program +} + +type Member interface { + name() string + isMember() +} + +type CommonMember struct { + Type string `yaml:"type" json:"type"` + Name string `yaml:"name" json:"name"` +} + +func (m CommonMember) name() string { + return m.Name +} + +func (CommonMember) isMember() {} + +type NamedConst struct { + CommonMember `yaml:",inline"` + Value NamedConstValue `yaml:"value" json:"value"` +} + +type NamedConstValue struct { + Type string `yaml:"type" json:"type"` + Value string `yaml:"value" json:"value"` +} + +type MemberGlobal struct { + CommonMember `yaml:",inline"` + Index int `yaml:"index" json:"index"` + GoType string `yaml:"go_type" json:"go_type"` +} + +type Function struct { + CommonMember `yaml:",inline"` + BasicBlocks []BasicBlock `yaml:"basic_blocks" json:"basic_blocks"` + Parameters []Parameter `yaml:"parameters" json:"parameters"` + FreeVars []Value `yaml:"free_vars" json:"free_vars"` + ReturnTypes []string `yaml:"return_types" json:"return_types"` + Recover *BasicBlock `yaml:"recover,omitempty" json:"recover,omitempty"` +} + +type BasicBlock struct { + Index int `yaml:"index" json:"index"` + Instructions []Instruction `yaml:"instructions" json:"instructions"` + Prev []int `yaml:"prev,flow" json:"prev"` + Next []int `yaml:"next,flow" json:"next"` +} + +type Instruction interface { + isInstruction() +} + +type CommonInstruction struct { + Type string `yaml:"type" json:"type"` + Name string `yaml:"name" json:"name"` + Block int `yaml:"block" json:"block"` + Line int `yaml:"line" json:"line"` +} + +func (CommonInstruction) isInstruction() {} + +type DebugRef struct { + CommonInstruction `yaml:",inline"` +} + +type UnOp struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Op string `yaml:"op" json:"op"` + Register string `yaml:"register" json:"register"` + Argument Value `yaml:"argument" json:"argument"` + CommaOk bool `yaml:"comma_ok" json:"comma_ok"` +} + +type BinOp struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Op string `yaml:"op" json:"op"` + Register string `yaml:"register" json:"register"` + First Value `yaml:"first" json:"first"` + Second Value `yaml:"second" json:"second"` +} + +type Call struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Value Value `yaml:"value" json:"value"` + Method string `yaml:"method" json:"method"` + Args []Value `yaml:"args" json:"args"` +} + +type ChangeInterface struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Value Value `yaml:"value" json:"value"` +} + +type ChangeType struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Value Value `yaml:"value" json:"value"` +} + +type Convert struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Value Value `yaml:"value" json:"value"` +} + +type SliceToArrayPointer struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Value Value `yaml:"value" json:"value"` +} + +type MakeInterface struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Value Value `yaml:"value" json:"value"` +} + +type Extract struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Tuple Value `yaml:"tuple" json:"tuple"` + Index int `yaml:"index" json:"index"` +} + +type SliceInst struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Collection Value `yaml:"collection" json:"collection"` + Low Value `yaml:"low" json:"low"` + High Value `yaml:"high" json:"high"` + Max Value `yaml:"max" json:"max"` +} + +type Return struct { + CommonInstruction `yaml:",inline"` + Results []Value `yaml:"results" json:"results"` +} + +type RunDefers struct { + CommonInstruction `yaml:",inline"` +} + +type Panic struct { + CommonInstruction `yaml:",inline"` + Value Value `yaml:"value" json:"value"` +} + +type Send struct { + CommonInstruction `yaml:",inline"` +} + +type Store struct { + CommonInstruction `yaml:",inline"` + Addr Value `yaml:"addr" json:"addr"` + Value Value `yaml:"value" json:"value"` +} + +type If struct { + CommonInstruction `yaml:",inline"` + Condition Value `yaml:"condition" json:"condition"` + TrueBranch int `yaml:"true_branch" json:"true_branch"` + FalseBranch int `yaml:"false_branch" json:"false_branch"` +} + +type Jump struct { + CommonInstruction `yaml:",inline"` + Index int `yaml:"index" json:"index"` +} + +type Defer struct { + CommonInstruction `yaml:",inline"` + Value Value `yaml:"value" json:"value"` + Method string `yaml:"method" json:"method"` + Args []Value `yaml:"args" json:"args"` +} + +type Go struct { + CommonInstruction `yaml:",inline"` +} + +type MakeChan struct { + CommonInstruction `yaml:",inline"` +} + +type Alloc struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Comment string `yaml:"comment" json:"comment"` +} + +type MakeSlice struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Len Value `yaml:"len" json:"len"` + Cap Value `yaml:"cap" json:"cap"` +} + +type MakeMap struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Reserve Value `yaml:"reserve" json:"reserve"` +} + +type Range struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Collection Value `yaml:"collection" json:"collection"` +} + +type Next struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Iter Value `yaml:"iter" json:"iter"` + IsString bool `yaml:"is_string" json:"is_string"` +} + +type FieldAddr struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Struct Value `yaml:"struct" json:"struct"` + Field int `yaml:"field" json:"field"` +} + +type Field struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Struct Value `yaml:"struct" json:"struct"` + Field int `yaml:"field" json:"field"` +} + +type IndexAddr struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Collection Value `yaml:"collection" json:"collection"` + Index Value `yaml:"index" json:"index"` +} + +type Index struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Collection Value `yaml:"collection" json:"collection"` + Index Value `yaml:"index" json:"index"` +} + +type Lookup struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Map Value `yaml:"map" json:"map"` + Key Value `yaml:"key" json:"key"` + CommaOk bool `yaml:"comma_ok" json:"comma_ok"` +} + +type MapUpdate struct { + CommonInstruction `yaml:",inline"` + Map Value `yaml:"map" json:"map"` + Key Value `yaml:"key" json:"key"` + Value Value `yaml:"value" json:"value"` +} + +type TypeAssert struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Value Value `yaml:"value" json:"value"` + AssertedType string `yaml:"asserted_type" json:"asserted_type"` +} + +type MakeClosure struct { + CommonInstruction `yaml:",inline"` + Register string `yaml:"register" json:"register"` + Function Value `yaml:"function" json:"function"` + Bindings []Value `yaml:"bindings" json:"bindings"` +} + +type Phi struct { + CommonInstruction `yaml:",inline"` + GoType string `yaml:"go_type" json:"go_type"` + Register string `yaml:"register" json:"register"` + Edges []Value `yaml:"edges" json:"edges"` +} + +type Select struct { + CommonInstruction `yaml:",inline"` +} + +type Value interface { + isValue() +} + +type CommonValue struct { + Type string `yaml:"type" json:"type"` + GoType string `yaml:"go_type" json:"go_type"` + Name string `yaml:"name" json:"name"` +} + +func (CommonValue) isValue() {} + +type Const struct { + CommonValue `yaml:",inline"` + Value NamedConstValue `yaml:"value" json:"value"` +} + +type Global struct { + CommonValue `yaml:",inline"` + Index int `yaml:"index" json:"index"` +} + +type Parameter struct { + CommonValue `yaml:",inline"` + Index int `yaml:"index" json:"index"` +} + +type FreeVar struct { + CommonValue `yaml:",inline"` + Index int `yaml:"index" json:"index"` +} + +type Type interface { + isType() +} + +type CommonType struct { + Type string `yaml:"type" json:"type"` + Name string `yaml:"name" json:"name"` +} + +func (CommonType) isType() {} + +type Alias struct { + CommonType `yaml:"-,inline"` + From string `yaml:"from" json:"from"` +} + +type Array struct { + CommonType `yaml:"-,inline"` + Len int64 `yaml:"len" json:"len"` + Elem string `yaml:"elem" json:"elem"` +} + +type Chan struct { + CommonType `yaml:"-,inline"` + Dir int `yaml:"dir" json:"dir"` + Elem string `yaml:"elem" json:"elem"` +} + +type Interface struct { + CommonType `yaml:"-,inline"` + Methods []string `yaml:"methods" json:"methods"` +} + +type Map struct { + CommonType `yaml:"-,inline"` + Key string `yaml:"key" json:"key"` + Elem string `yaml:"elem" json:"elem"` +} + +type Named struct { + CommonType `yaml:"-,inline"` + Underlying string `yaml:"underlying" json:"underlying"` + Methods []string `yaml:"methods" json:"methods"` +} + +type Pointer struct { + CommonType `yaml:"-,inline"` + Elem string `yaml:"elem" json:"elem"` +} + +type Signature struct { + CommonType `yaml:"-,inline"` + Params []string `yaml:"params" json:"params"` + Results []string `yaml:"results" json:"results"` +} + +type Slice struct { + CommonType `yaml:"-,inline"` + Elem string `yaml:"elem" json:"elem"` +} + +type Struct struct { + CommonType `yaml:"-,inline"` + Fields []string `yaml:"fields" json:"fields"` +} + +type Tuple struct { + CommonType `yaml:"-,inline"` + Elems []string `yaml:"elems" json:"elems"` +} diff --git a/usvm-go/src/main/go/pack.go b/usvm-go/src/main/go/pack.go new file mode 100644 index 0000000000..29541e1b62 --- /dev/null +++ b/usvm-go/src/main/go/pack.go @@ -0,0 +1,711 @@ +package main + +import ( + "go/constant" + "go/types" + "log" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/tools/go/ssa" +) + +func PackPackage(in *ssa.Package) Package { + p := Package{ + Name: in.Pkg.Path(), + Types: make(map[string]Type), + program: in.Prog, + } + p.PackMembers(in.Members) + sort.Slice(p.Members, func(i, j int) bool { + return p.Members[i].name() < p.Members[j].name() + }) + return p +} + +func (p *Package) AddType(typ types.Type) { + name := typ.String() + if p.Types[name] != nil { + return + } + + common := CommonType{ + Name: name, + } + + switch t := typ.(type) { + case *types.Alias: + common.Type = AliasType + p.Types[name] = Alias{ + CommonType: common, + From: types.Unalias(t).String(), + } + p.AddType(types.Unalias(t)) + case *types.Array: + common.Type = ArrayType + p.Types[name] = Array{ + CommonType: common, + Len: t.Len(), + Elem: t.Elem().String(), + } + p.AddType(t.Elem()) + case *types.Basic: + common.Type = BasicType + p.Types[name] = common + case *types.Chan: + common.Type = ChanType + p.Types[name] = Chan{ + CommonType: common, + Dir: int(t.Dir()), + Elem: t.Elem().String(), + } + p.AddType(t.Elem()) + case *types.Interface: + common.Type = InterfaceType + p.Types[name] = Interface{ + CommonType: common, + Methods: p.PackMethods(t), + } + case *types.Map: + common.Type = MapType + p.Types[name] = Map{ + CommonType: common, + Key: t.Key().String(), + Elem: t.Elem().String(), + } + p.AddType(t.Key()) + p.AddType(t.Elem()) + case *types.Named: + common.Type = NamedType + p.Types[name] = Named{ + CommonType: common, + Underlying: t.Underlying().String(), + Methods: p.PackMethods(t), + } + // add members after adding type to avoid stack overflow + for i := 0; i < t.NumMethods(); i++ { + f := p.program.FuncValue(t.Method(i)) + if f == nil { + continue + } + p.Members = append(p.Members, p.PackMember(f)) + } + p.AddType(t.Underlying()) + case *types.Pointer: + common.Type = PointerType + p.Types[name] = Pointer{ + CommonType: common, + Elem: t.Elem().String(), + } + p.AddType(t.Elem()) + case *types.Signature: + common.Type = SignatureType + p.Types[name] = Signature{ + CommonType: common, + Params: p.PackTypeTuple(t.Params()), + Results: p.PackTypeTuple(t.Results()), + } + case *types.Slice: + common.Type = SliceType + p.Types[name] = Slice{ + CommonType: common, + Elem: t.Elem().String(), + } + p.AddType(t.Elem()) + case *types.Struct: + fields := make([]string, 0) + for i := 0; i < t.NumFields(); i++ { + field := t.Field(i) + fields = append(fields, field.Type().String()) + p.AddType(field.Type()) + } + common.Type = StructType + p.Types[name] = Struct{ + CommonType: common, + Fields: fields, + } + case *types.Tuple: + common.Type = TupleType + p.Types[name] = Tuple{ + CommonType: common, + Elems: p.PackTypeTuple(t), + } + case *types.TypeParam: + common.Type = TypeParamType + p.Types[name] = common + case *types.Union: + common.Type = UnionType + p.Types[name] = common + default: + common.Type = OpaqueType + p.Types[name] = common + } +} + +func (p *Package) PackMembers(membersMap map[string]ssa.Member) { + members := lo.Values(membersMap) + for _, member := range membersMap { + if f, ok := member.(*ssa.Function); ok { + members = append(members, FindFunctions(f)...) + } + } + + for _, member := range members { + p.Members = append(p.Members, p.PackMember(member)) + } +} + +func (p *Package) PackMember(in ssa.Member) Member { + common := CommonMember{ + Name: in.Name(), + } + + switch member := in.(type) { + case *ssa.NamedConst: + p.AddType(member.Value.Type()) + common.Type = NamedConstMember + return NamedConst{ + CommonMember: common, + Value: NamedConstValue{ + Type: member.Value.Type().String(), + Value: member.Value.Value.String(), + }, + } + case *ssa.Global: + p.AddType(member.Type()) + common.Type = GlobalMember + common.Name = member.Pkg.Pkg.Path() + "." + common.Name + return MemberGlobal{ + CommonMember: common, + Index: int(member.Pos()), + GoType: member.Type().String(), + } + case *ssa.Type: + p.AddType(member.Type()) + common.Type = TypeMember + return common + case *ssa.Function: + var recoverBlock *BasicBlock + if member.Recover != nil { + recoverBlock = lo.ToPtr(p.PackBasicBlock(member.Recover, 0)) + } + common.Type = FunctionMember + common.Name = p.FunctionName(member) + start := 0 + basicBlocks := make([]BasicBlock, len(member.Blocks)) + for i, b := range member.Blocks { + basicBlocks[i] = p.PackBasicBlock(b, start) + start += len(b.Instrs) + } + return Function{ + CommonMember: common, + BasicBlocks: basicBlocks, + Parameters: lo.Map(member.Params, p.PackParameter), + FreeVars: lo.Map(member.FreeVars, func(v *ssa.FreeVar, _ int) Value { return p.PackValue(v) }), + ReturnTypes: p.PackReturnTypes(member), + Recover: recoverBlock, + } + } + + return nil +} + +func (p *Package) PackBasicBlock(in *ssa.BasicBlock, start int) BasicBlock { + instructions := make([]Instruction, len(in.Instrs)) + for i, inst := range in.Instrs { + instructions[i] = p.PackInstruction(inst, start+i) + } + return BasicBlock{ + Index: in.Index, + Instructions: instructions, + Prev: lo.Map(in.Preds, func(b *ssa.BasicBlock, _ int) int { return b.Index }), + Next: lo.Map(in.Succs, func(b *ssa.BasicBlock, _ int) int { return b.Index }), + } +} + +func (p *Package) PackParameter(in *ssa.Parameter, index int) Parameter { + p.AddType(in.Type()) + return Parameter{ + CommonValue: CommonValue{ + Type: ParameterValue, + GoType: in.Type().String(), + Name: in.Name(), + }, + Index: index, + } +} + +func (p *Package) PackReturnTypes(in *ssa.Function) []string { + return p.PackTypeTuple(in.Signature.Results()) +} + +func (p *Package) PackTypeTuple(in *types.Tuple) []string { + returnTypes := make([]string, 0, in.Len()) + for i := 0; i < in.Len(); i++ { + p.AddType(in.At(i).Type()) + returnTypes = append(returnTypes, in.At(i).Type().String()) + } + + return returnTypes +} + +func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { + common := CommonInstruction{ + Name: in.String(), + Block: in.Block().Index, + Line: index, + } + if typed, ok := in.(Typed); ok { + p.AddType(typed.Type()) + } + + switch inst := in.(type) { + case *ssa.DebugRef: + common.Type = DebugRefInstruction + return DebugRef{ + CommonInstruction: common, + } + case *ssa.UnOp: + common.Type = UnOpInstruction + return UnOp{ + CommonInstruction: common, + GoType: inst.Type().String(), + Op: inst.Op.String(), + Register: inst.Name(), + Argument: p.PackValue(inst.X), + CommaOk: inst.CommaOk, + } + case *ssa.BinOp: + common.Type = BinOpInstruction + return BinOp{ + CommonInstruction: common, + GoType: inst.Type().String(), + Op: inst.Op.String(), + Register: inst.Name(), + First: p.PackValue(inst.X), + Second: p.PackValue(inst.Y), + } + case *ssa.Call: + common.Type = CallInstruction + method := "" + if inst.Call.IsInvoke() { + method = inst.Call.Method.Name() + } + return Call{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Value: p.PackValue(inst.Call.Value), + Method: method, + Args: lo.Map(inst.Call.Args, p.PackValueIdx), + } + case *ssa.ChangeInterface: + common.Type = ChangeInterfaceInstruction + return ChangeInterface{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Value: p.PackValue(inst.X), + } + case *ssa.ChangeType: + common.Type = ChangeTypeInstruction + return ChangeType{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Value: p.PackValue(inst.X), + } + case *ssa.Convert: + common.Type = ConvertInstruction + return Convert{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Value: p.PackValue(inst.X), + } + case *ssa.SliceToArrayPointer: + common.Type = SliceToArrayPointerInstruction + return SliceToArrayPointer{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Value: p.PackValue(inst.X), + } + case *ssa.MakeInterface: + common.Type = MakeInterfaceInstruction + return MakeInterface{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Value: p.PackValue(inst.X), + } + case *ssa.Extract: + common.Type = ExtractInstruction + return Extract{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Tuple: p.PackValue(inst.Tuple), + Index: inst.Index, + } + case *ssa.Slice: + var ( + low ssa.Value = ssa.NewConst(constant.MakeInt64(0), types.Typ[types.Int]) + high ssa.Value = ssa.NewConst(constant.MakeInt64(-1), types.Typ[types.Int]) + mx ssa.Value = ssa.NewConst(constant.MakeInt64(-1), types.Typ[types.Int]) + ) + if inst.Low != nil { + low = inst.Low + } + if inst.High != nil { + high = inst.High + } + if inst.Max != nil { + mx = inst.Max + } + common.Type = SliceInstruction + return SliceInst{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Collection: p.PackValue(inst.X), + Low: p.PackValue(low), + High: p.PackValue(high), + Max: p.PackValue(mx), + } + case *ssa.Return: + common.Type = ReturnInstruction + return Return{ + CommonInstruction: common, + Results: lo.Map(inst.Results, p.PackValueIdx), + } + case *ssa.RunDefers: + common.Type = RunDefersInstruction + return RunDefers{ + CommonInstruction: common, + } + case *ssa.Panic: + common.Type = PanicInstruction + return Panic{ + CommonInstruction: common, + Value: p.PackValue(inst.X), + } + case *ssa.Send: + common.Type = SendInstruction + return Send{ + CommonInstruction: common, + } + case *ssa.Store: + common.Type = StoreInstruction + return Store{ + CommonInstruction: common, + Addr: p.PackValue(inst.Addr), + Value: p.PackValue(inst.Val), + } + case *ssa.If: + common.Type = IfInstruction + return If{ + CommonInstruction: common, + Condition: p.PackValue(inst.Cond), + TrueBranch: inst.Block().Succs[0].Index, + FalseBranch: inst.Block().Succs[1].Index, + } + case *ssa.Jump: + common.Type = JumpInstruction + return Jump{ + CommonInstruction: common, + Index: inst.Block().Succs[0].Index, + } + case *ssa.Defer: + common.Type = DeferInstruction + method := "" + if inst.Call.IsInvoke() { + method = inst.Call.Method.Name() + } + return Defer{ + CommonInstruction: common, + Value: p.PackValue(inst.Call.Value), + Method: method, + Args: lo.Map(inst.Call.Args, p.PackValueIdx), + } + case *ssa.Go: + common.Type = GoInstruction + return Go{ + CommonInstruction: common, + } + case *ssa.MakeChan: + common.Type = MakeChanInstruction + return MakeChan{ + CommonInstruction: common, + } + case *ssa.Alloc: + common.Type = AllocInstruction + goType := inst.Type().Underlying().(*types.Pointer).Elem() + p.AddType(goType) + return Alloc{ + CommonInstruction: common, + GoType: goType.String(), + Register: inst.Name(), + Comment: inst.Comment, + } + case *ssa.MakeSlice: + common.Type = MakeSliceInstruction + return MakeSlice{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Len: p.PackValue(inst.Len), + Cap: p.PackValue(inst.Cap), + } + case *ssa.MakeMap: + common.Type = MakeMapInstruction + reserve := p.PackValue(ssa.NewConst(constant.MakeUint64(0), types.Typ[types.Uint32])) + if inst.Reserve != nil { + reserve = p.PackValue(inst.Reserve) + } + return MakeMap{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Reserve: reserve, + } + case *ssa.Range: + common.Type = RangeInstruction + return Range{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Collection: p.PackValue(inst.X), + } + case *ssa.Next: + common.Type = NextInstruction + return Next{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Iter: p.PackValue(inst.Iter), + IsString: inst.IsString, + } + case *ssa.FieldAddr: + common.Type = FieldAddrInstruction + return FieldAddr{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Struct: p.PackValue(inst.X), + Field: inst.Field, + } + case *ssa.Field: + common.Type = FieldInstruction + return Field{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Struct: p.PackValue(inst.X), + Field: inst.Field, + } + case *ssa.IndexAddr: + common.Type = IndexAddrInstruction + return IndexAddr{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Collection: p.PackValue(inst.X), + Index: p.PackValue(inst.Index), + } + case *ssa.Index: + common.Type = IndexInstruction + return Index{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Collection: p.PackValue(inst.X), + Index: p.PackValue(inst.Index), + } + case *ssa.Lookup: + common.Type = LookupInstruction + return Lookup{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Map: p.PackValue(inst.X), + Key: p.PackValue(inst.Index), + CommaOk: inst.CommaOk, + } + case *ssa.MapUpdate: + common.Type = MapUpdateInstruction + return MapUpdate{ + CommonInstruction: common, + Map: p.PackValue(inst.Map), + Key: p.PackValue(inst.Key), + Value: p.PackValue(inst.Value), + } + case *ssa.TypeAssert: + common.Type = TypeAssertInstruction + return TypeAssert{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Value: p.PackValue(inst.X), + AssertedType: inst.AssertedType.String(), + } + case *ssa.MakeClosure: + common.Type = MakeClosureInstruction + return MakeClosure{ + CommonInstruction: common, + Register: inst.Name(), + Function: p.PackValue(inst.Fn), + Bindings: lo.Map(inst.Bindings, p.PackValueIdx), + } + case *ssa.Phi: + common.Type = PhiInstruction + return Phi{ + CommonInstruction: common, + GoType: inst.Type().String(), + Register: inst.Name(), + Edges: lo.Map(inst.Edges, p.PackValueIdx), + } + case *ssa.Select: + common.Type = SelectInstruction + return Select{ + CommonInstruction: common, + } + case *ssa.MultiConvert: + common.Type = MultiConvertInstruction + return common + default: + log.Fatalf("unexpected instruction: %T\n", inst) + } + return nil +} + +func (p *Package) PackValue(in ssa.Value) Value { + p.AddType(in.Type()) + common := CommonValue{ + GoType: in.Type().String(), + Name: in.Name(), + } + + switch value := in.(type) { + case *ssa.Const: + common.Type = ConstValue + v := "nil" + if value.Value != nil { + v = value.Value.String() + } + return Const{ + CommonValue: common, + Value: NamedConstValue{ + Type: value.Type().String(), + Value: v, + }, + } + case *ssa.Global: + common.Type = GlobalValue + common.Name = value.Pkg.Pkg.Path() + "." + common.Name + return Global{ + CommonValue: common, + Index: int(value.Pos()), + } + case *ssa.Parameter: + common.Type = ParameterValue + return Parameter{ + CommonValue: common, + Index: FindParameterIndex(value), + } + case *ssa.FreeVar: + common.Type = FreeVarValue + return FreeVar{ + CommonValue: common, + Index: FindFreeVarIndex(value), + } + case *ssa.Function: + common.Type = FunctionValue + common.Name = p.FunctionName(value) + return common + case *ssa.MakeClosure: + common.Type = MakeClosureValue + return common + case *ssa.Builtin: + common.Type = BuiltinValue + return common + default: + common.Type = VarValue + return common + } +} + +func (p *Package) PackValueIdx(in ssa.Value, _ int) Value { + return p.PackValue(in) +} + +func (p *Package) PackMethods(in WithMethods) []string { + methods := make([]string, 0) + for i := 0; i < in.NumMethods(); i++ { + method := in.Method(i) + signature := method.Type().(*types.Signature) + methods = append(methods, method.Name()+p.PackMethodParams(signature.Params())+p.PackMethodResults(signature.Results())) + } + + return methods +} + +func (p *Package) PackMethodParams(in *types.Tuple) string { + return "(" + strings.Join(p.PackTupleTypes(in), ", ") + ")" +} + +func (p *Package) PackMethodResults(in *types.Tuple) string { + results := p.PackTupleTypes(in) + out := strings.Join(results, ", ") + if in.Len() > 0 { + if in.Len() > 1 { + out = "(" + out + ")" + } + out = " " + out + } + return out +} + +func (p *Package) PackTupleTypes(in *types.Tuple) []string { + out := make([]string, 0) + for i := 0; i < in.Len(); i++ { + out = append(out, in.At(i).Type().String()) + } + return out +} + +func (p *Package) FunctionName(in *ssa.Function) string { + return strings.TrimPrefix(in.String(), p.Name+".") +} + +func FindParameterIndex(in *ssa.Parameter) int { + _, index, _ := lo.FindIndexOf(in.Parent().Params, func(other *ssa.Parameter) bool { + return other == in + }) + return index +} + +func FindFreeVarIndex(in *ssa.FreeVar) int { + _, index, _ := lo.FindIndexOf(in.Parent().FreeVars, func(other *ssa.FreeVar) bool { + return other == in + }) + return index +} + +func FindFunctions(in *ssa.Function) []ssa.Member { + members := lo.Map(in.AnonFuncs, func(f *ssa.Function, _ int) ssa.Member { return f }) + for _, f := range in.AnonFuncs { + members = append(members, FindFunctions(f)...) + } + return members +} + +type Typed interface { + Type() types.Type +} + +type WithMethods interface { + NumMethods() int + Method(i int) *types.Func +} diff --git a/usvm-go/src/main/go/ssa.go b/usvm-go/src/main/go/ssa.go new file mode 100644 index 0000000000..ec69a5d3a2 --- /dev/null +++ b/usvm-go/src/main/go/ssa.go @@ -0,0 +1,129 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "go/token" + "io" + "log" + "os" + "path" + "strings" + "time" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" + "gopkg.in/yaml.v3" +) + +type Config struct { + EnableTracing bool `yaml:"enable_tracing"` + + DumpSSA bool `yaml:"dump_ssa"` + DumpSSAFileName string `yaml:"dump_ssa_file_name"` +} + +type SSA struct { + output io.WriteCloser + + program *ssa.Program + packages []*ssa.Package +} + +func NewSSA(packageName string, cfg Config) (*SSA, error) { + mode := packages.NeedName | + packages.NeedFiles | + packages.NeedCompiledGoFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedExportFile | + packages.NeedTypes | + packages.NeedTypesSizes | + packages.NeedTypesInfo | + packages.NeedSyntax | + packages.NeedModule | + packages.NeedEmbedFiles | + packages.NeedEmbedPatterns + packageCfg := &packages.Config{Mode: mode} + if cfg.EnableTracing { + packageCfg.Logf = log.Printf + } + + initialPackages, err := packages.Load(packageCfg, packageName) + if err != nil { + return nil, err + } + if len(initialPackages) == 0 { + return nil, fmt.Errorf("no packages were loaded") + } + if packages.PrintErrors(initialPackages) > 0 { + return nil, fmt.Errorf("packages contain errors") + } + + program, _ := ssautil.AllPackages(initialPackages, ssa.InstantiateGenerics|ssa.SanityCheckFunctions) + program.Build() + + s := &SSA{ + output: os.Stdout, + program: program, + packages: program.AllPackages(), + } + + if cfg.DumpSSA { + if cfg.DumpSSAFileName != "" { + CheckError(os.MkdirAll(path.Dir(cfg.DumpSSAFileName), os.ModePerm)) + s.output, err = os.Create(cfg.DumpSSAFileName) + if err != nil { + return nil, err + } + defer CheckClose(s.output) + } + s.dump() + } + + return s, nil +} + +func (s *SSA) Write() { + for _, pkg := range s.packages { + now := time.Now() + s.writePackage(pkg) + log.Printf("%s serialized! Took %.3f seconds\n", pkg.String(), time.Since(now).Seconds()) + } +} + +func (s *SSA) writePackage(p *ssa.Package) { + for _, ext := range []string{".yaml", ".json"} { + CheckError(os.MkdirAll(OutputDir, os.ModePerm)) + output, err := os.Create(path.Join(OutputDir, strings.ReplaceAll(p.Pkg.Path(), "/", "_")+ext)) + CheckError(err) + + buf := &bytes.Buffer{} + pkg := PackPackage(p) + if ext == ".yaml" { + CheckError(yaml.NewEncoder(buf).Encode(pkg)) + } else { + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + CheckError(enc.Encode(pkg)) + buf.Truncate(buf.Len() - 1) // remove newline character + } + CheckError(output.Write(buf.Bytes())) + CheckClose(output) + } +} + +func (s *SSA) dump() { + out := &bytes.Buffer{} + for _, pkg := range s.packages { + ssa.WritePackage(out, pkg) + for _, object := range pkg.Members { + if object.Token() == token.FUNC { + ssa.WriteFunction(out, pkg.Func(object.Name())) + } + } + } + CheckError(s.output.Write(out.Bytes())) +} diff --git a/usvm-go/src/main/go/util.go b/usvm-go/src/main/go/util.go new file mode 100644 index 0000000000..eaa7906145 --- /dev/null +++ b/usvm-go/src/main/go/util.go @@ -0,0 +1,31 @@ +package main + +import ( + "io" + "log" + "path" + "path/filepath" + "runtime" +) + +var ( + _, filename, _, _ = runtime.Caller(0) + + Root = filepath.Dir(filename) + OutputDir = path.Join(Root, filepath.Dir("../../../out/")) +) + +func CheckClose(c io.Closer) { + CheckError(c.Close()) +} + +func CheckError(args ...any) { + for _, arg := range args { + if arg == nil { + continue + } + if err, ok := arg.(error); ok && err != nil { + log.Fatalf("Fatal: %v", err) + } + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt b/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt new file mode 100644 index 0000000000..0d464367b0 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt @@ -0,0 +1,53 @@ +package org.usvm + +import org.jacodb.go.api.GoCallInst +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoMethod +import org.usvm.statistics.ApplicationGraph + +class GoApplicationGraph : ApplicationGraph { + override fun predecessors(node: GoInst): Sequence { + val graph = node.location.method.flowGraph() + val predecessors = graph.predecessors(node) + val throwers = graph.throwers(node) + return predecessors.asSequence() + throwers.asSequence() + } + + override fun successors(node: GoInst): Sequence { + val graph = node.location.method.flowGraph() + val successors = graph.successors(node) + return successors.asSequence() + } + + override fun callees(node: GoInst): Sequence { + if (node !is GoCallInst) { + return emptySequence() + } + val callExpr = node.callExpr + return sequenceOf(callExpr.callee!!) + } + + override fun callers(method: GoMethod): Sequence { + var res = listOf() + for (block in method.blocks) { + res = listOf(res, block.instructions).flatten() + } + return res.asSequence() + } + + override fun entryPoints(method: GoMethod): Sequence { + return method.flowGraph().entries.asSequence() + } + + override fun exitPoints(method: GoMethod): Sequence { + return method.flowGraph().exits.asSequence() + } + + override fun methodOf(node: GoInst): GoMethod { + return node.location.method + } + + override fun statementsOf(method: GoMethod): Sequence { + return method.flowGraph().instructions.asSequence() + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt b/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt new file mode 100644 index 0000000000..937ceb762d --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt @@ -0,0 +1,35 @@ +package org.usvm + +import io.ksmt.solver.yices.KYicesSolver +import io.ksmt.solver.z3.KZ3Solver +import org.jacodb.go.api.GoType +import org.usvm.solver.USolverBase +import org.usvm.solver.UTypeSolver +import org.usvm.type.GoTypeSystem +import org.usvm.types.UTypeSystem + +class GoComponents( + private val typeSystem: GoTypeSystem, + private val options: UMachineOptions, +): UComponents { + override val useSolverForForks: Boolean get() = options.useSolverForForks + + override fun mkTypeSystem(ctx: UContext): UTypeSystem { + return typeSystem + } + + override fun > mkSolver(ctx: Context): USolverBase { + val (translator, decoder) = buildTranslatorAndLazyDecoder(ctx) + val solver = when (options.solverType) { + SolverType.YICES -> KYicesSolver(ctx) + SolverType.Z3 -> KZ3Solver(ctx) + } + val typeSolver = UTypeSolver(typeSystem) + + return USolverBase(ctx, solver, typeSolver, translator, decoder, options.solverTimeout) + } + + override fun > mkSizeExprProvider(ctx: Context): USizeExprProvider { + return UBv32SizeExprProvider(ctx) + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/GoContext.kt b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt new file mode 100644 index 0000000000..39c77ba23b --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt @@ -0,0 +1,94 @@ +package org.usvm + +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.GoAssignInst +import org.jacodb.go.api.GoFunction +import org.jacodb.go.api.GoGlobal +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.NullType +import org.jacodb.go.api.PointerType +import org.usvm.operator.GoUnaryOperator +import org.usvm.type.GoBasicTypes +import org.usvm.type.GoVoidSort +import org.usvm.type.GoVoidValue + +class GoContext( + components: UComponents, +) : UContext(components) { + val voidSort by lazy { GoVoidSort(this) } + + val voidValue by lazy { GoVoidValue(this) } + + val noValue = mkConst("nothing", voidSort) + + private val methodInfo: MutableMap = hashMapOf() + private val globals: MutableMap> = hashMapOf() + + fun getMethodInfo(method: GoMethod) = methodInfo[method]!! + + fun setMethodInfo(method: GoMethod) { + val localsCount = method.blocks.flatMap { it.instructions }.filterIsInstance().size + val argumentsCount = method.parameters.size + + setMethodInfo(method, GoMethodInfo(localsCount, argumentsCount, emptyArray())) + } + + fun setMethodInfo(method: GoMethod, parameters: Array>) { + val localsCount = method.blocks.flatMap { it.instructions }.filterIsInstance().size + val freeVariablesCount = getFreeVariablesCount(method) + setMethodInfo(method, GoMethodInfo(localsCount + freeVariablesCount, parameters.size, parameters)) + } + + fun addGlobal(global: GoGlobal, expr: UExpr) { + globals[global] = expr + } + + fun getGlobal(global: GoGlobal): UExpr = globals[global]!! + + fun freeVariableOffset(method: GoMethod) = getArgsCount(method) + + fun localVariableOffset(method: GoMethod) = getArgsCount(method) + getFreeVariablesCount(method) + + fun typeToSort(type: GoType): USort = when (type) { + is BasicType -> basicTypeToSort(type) + is NamedType -> addressSort + is PointerType -> addressSort + is NullType -> voidSort + else -> addressSort + } + + fun mkPrimitiveCast(expr: UExpr, to: USort): UExpr = when (to) { + boolSort -> GoUnaryOperator.CastToBool(expr) + bv8Sort -> GoUnaryOperator.CastToInt8(expr) + bv16Sort -> GoUnaryOperator.CastToInt16(expr) + bv32Sort -> GoUnaryOperator.CastToInt32(expr) + bv64Sort -> GoUnaryOperator.CastToInt64(expr) + fp32Sort -> GoUnaryOperator.CastToFloat32(expr) + fp64Sort -> GoUnaryOperator.CastToFloat64(expr) + else -> throw IllegalStateException() + } + + private fun setMethodInfo(method: GoMethod, info: GoMethodInfo) { + methodInfo[method] = info + } + + private fun getArgsCount(method: GoMethod): Int = methodInfo[method]!!.argumentsCount + + private fun getFreeVariablesCount(method: GoMethod): Int = when (method) { + is GoFunction -> method.freeVars.size + else -> 0 + } + + private fun basicTypeToSort(type: BasicType): USort = when (type) { + GoBasicTypes.BOOL -> boolSort + GoBasicTypes.INT, GoBasicTypes.UINT, GoBasicTypes.INT32, GoBasicTypes.UINT32, GoBasicTypes.RUNE -> bv32Sort + GoBasicTypes.INT8, GoBasicTypes.UINT8 -> bv8Sort + GoBasicTypes.INT16, GoBasicTypes.UINT16 -> bv16Sort + GoBasicTypes.INT64, GoBasicTypes.UINT64, GoBasicTypes.UINTPTR, GoBasicTypes.UNSAFE_POINTER -> bv64Sort + GoBasicTypes.FLOAT32 -> fp32Sort + GoBasicTypes.FLOAT64 -> fp64Sort + else -> addressSort + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt new file mode 100644 index 0000000000..d9b5dda5a6 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt @@ -0,0 +1,69 @@ +package org.usvm + +import org.jacodb.go.api.GoGlobal +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoInstLocation +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoType +import org.usvm.api.UnknownMethodException +import org.usvm.api.UnknownPackageException +import org.usvm.util.isInit +import org.usvm.util.isOsInit + +class GoMethodInfo( + val variablesCount: Int, + val argumentsCount: Int, + val arguments: Array> +) { + override fun toString(): String { + return "variables: $variablesCount, arguments: $argumentsCount" + } +} + +class GoCall( + val method: GoMethod, + val entrypoint: GoInst, +) + +class GoPackage( + val name: String, + val methods: List, + val globals: List, + val types: Map +) { + private val methodsIndex: Map = methods.associateBy { it.metName } + + fun findMethod(name: String): GoMethod { + return methodsIndex[name] ?: throw UnknownMethodException(name) + } +} + +class GoProgram(packages: List) { + val globals = packages.flatMap { it.globals } + val types = packages.flatMap { it.types.entries }.associate { it.key to it.value } + + private val packagesIndex: Map = packages.associateBy { it.name } + + fun findPackage(name: String): GoPackage { + return packagesIndex[name] ?: throw UnknownPackageException(name) + } + + fun findMethod(location: GoInstLocation, name: String): GoMethod { + if (name.contains('.') && !name.contains('(')) { + val split = name.split('.') + val packageName = split[0] + val methodName = split[1] + return findPackage(packageName).findMethod(methodName) + } + + return findPackage(location.method.packageName).findMethod(name) + } + + fun findInitMethods(packageName: String): List { + return packagesIndex[packageName]!!.methods.filter { it.isInit() }.sortedBy { it.metName } + } + + fun findOsInitMethods(): List { + return packagesIndex.flatMap { it.value.methods }.filter { it.isOsInit() }.sortedBy { it.metName } + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt new file mode 100644 index 0000000000..13013c7e9e --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -0,0 +1,1082 @@ +package org.usvm + +import io.ksmt.expr.KBitVec32Value +import io.ksmt.expr.KConst +import io.ksmt.utils.asExpr +import org.jacodb.go.api.ArrayType +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.GoAddExpr +import org.jacodb.go.api.GoAllocExpr +import org.jacodb.go.api.GoAndExpr +import org.jacodb.go.api.GoAndNotExpr +import org.jacodb.go.api.GoBinaryExpr +import org.jacodb.go.api.GoBool +import org.jacodb.go.api.GoBuiltin +import org.jacodb.go.api.GoCallExpr +import org.jacodb.go.api.GoChangeInterfaceExpr +import org.jacodb.go.api.GoChangeTypeExpr +import org.jacodb.go.api.GoConst +import org.jacodb.go.api.GoConvertExpr +import org.jacodb.go.api.GoDivExpr +import org.jacodb.go.api.GoEqlExpr +import org.jacodb.go.api.GoExprVisitor +import org.jacodb.go.api.GoExtractExpr +import org.jacodb.go.api.GoFieldAddrExpr +import org.jacodb.go.api.GoFieldExpr +import org.jacodb.go.api.GoFloat32 +import org.jacodb.go.api.GoFloat64 +import org.jacodb.go.api.GoFreeVar +import org.jacodb.go.api.GoFunction +import org.jacodb.go.api.GoGeqExpr +import org.jacodb.go.api.GoGlobal +import org.jacodb.go.api.GoGtrExpr +import org.jacodb.go.api.GoIndexAddrExpr +import org.jacodb.go.api.GoIndexExpr +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoInt +import org.jacodb.go.api.GoInt16 +import org.jacodb.go.api.GoInt32 +import org.jacodb.go.api.GoInt64 +import org.jacodb.go.api.GoInt8 +import org.jacodb.go.api.GoLeqExpr +import org.jacodb.go.api.GoLookupExpr +import org.jacodb.go.api.GoLssExpr +import org.jacodb.go.api.GoMakeChanExpr +import org.jacodb.go.api.GoMakeClosureExpr +import org.jacodb.go.api.GoMakeInterfaceExpr +import org.jacodb.go.api.GoMakeMapExpr +import org.jacodb.go.api.GoMakeSliceExpr +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoModExpr +import org.jacodb.go.api.GoMulExpr +import org.jacodb.go.api.GoMultiConvertExpr +import org.jacodb.go.api.GoNeqExpr +import org.jacodb.go.api.GoNextExpr +import org.jacodb.go.api.GoNullConstant +import org.jacodb.go.api.GoOrExpr +import org.jacodb.go.api.GoParameter +import org.jacodb.go.api.GoPhiExpr +import org.jacodb.go.api.GoRangeExpr +import org.jacodb.go.api.GoSelectExpr +import org.jacodb.go.api.GoShlExpr +import org.jacodb.go.api.GoShrExpr +import org.jacodb.go.api.GoSliceExpr +import org.jacodb.go.api.GoSliceToArrayPointerExpr +import org.jacodb.go.api.GoStringConstant +import org.jacodb.go.api.GoSubExpr +import org.jacodb.go.api.GoType +import org.jacodb.go.api.GoTypeAssertExpr +import org.jacodb.go.api.GoUInt +import org.jacodb.go.api.GoUInt16 +import org.jacodb.go.api.GoUInt32 +import org.jacodb.go.api.GoUInt64 +import org.jacodb.go.api.GoUInt8 +import org.jacodb.go.api.GoUnArrowExpr +import org.jacodb.go.api.GoUnMulExpr +import org.jacodb.go.api.GoUnNotExpr +import org.jacodb.go.api.GoUnSubExpr +import org.jacodb.go.api.GoUnXorExpr +import org.jacodb.go.api.GoUnaryExpr +import org.jacodb.go.api.GoValue +import org.jacodb.go.api.GoVar +import org.jacodb.go.api.GoXorExpr +import org.jacodb.go.api.MapType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.NullType +import org.jacodb.go.api.PointerType +import org.jacodb.go.api.SignatureType +import org.jacodb.go.api.SliceType +import org.jacodb.go.api.TupleType +import org.usvm.api.UnknownBinaryOperationException +import org.usvm.api.UnknownFunctionException +import org.usvm.api.UnknownUnaryOperationException +import org.usvm.api.UnsupportedUnaryOperationException +import org.usvm.api.allocateArray +import org.usvm.api.collection.ObjectMapCollectionApi.ensureObjectMapSizeCorrect +import org.usvm.api.collection.ObjectMapCollectionApi.mkSymbolicObjectMap +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapAnyKey +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapGet +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapMergeInto +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapRemove +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapSize +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapAnyKey +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapGet +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapMergeInto +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapRemove +import org.usvm.api.memcpy +import org.usvm.api.readArrayIndex +import org.usvm.api.readArrayLength +import org.usvm.api.readField +import org.usvm.api.refSetContainsElement +import org.usvm.api.setContainsElement +import org.usvm.api.typeStreamOf +import org.usvm.api.writeArrayIndex +import org.usvm.api.writeField +import org.usvm.collection.array.UArrayIndexLValue +import org.usvm.collection.field.UFieldLValue +import org.usvm.collection.map.length.UMapLengthLValue +import org.usvm.collection.map.primitive.UMapEntryLValue +import org.usvm.collection.map.ref.URefMapEntryLValue +import org.usvm.interpreter.GoStepScope +import org.usvm.memory.ULValue +import org.usvm.memory.URegisterStackLValue +import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.operator.GoBinaryOperator +import org.usvm.operator.GoUnaryOperator +import org.usvm.operator.mkNarrow +import org.usvm.state.GoMethodResult +import org.usvm.state.GoState.Companion.POINTER_FIELD +import org.usvm.statistics.ApplicationGraph +import org.usvm.type.GoBasicTypes +import org.usvm.type.underlying +import org.usvm.types.first +import org.usvm.util.isInit + +class GoExprVisitor( + private val ctx: GoContext, + private val program: GoProgram, + private val scope: GoStepScope, + private val applicationGraph: ApplicationGraph, +) : GoExprVisitor> { + override fun visitGoCallExpr(expr: GoCallExpr): UExpr { + val func = expr.value + if (func is GoBuiltin) { + return callBuiltin(func, expr.args, expr.type) + } + if (func is GoParameter && expr.callee == null) { + return mockCall(expr, func) + } + + val result = scope.calcOnState { methodResult } + if (result is GoMethodResult.Success) { + scope.doWithState { methodResult = GoMethodResult.NoCall } + if (result.method.isInit(expr.location)) { + return ctx.noValue + } + return result.value + } + + val args = expr.args.let { if (expr.callee == null) it else listOf(func) + it } + val method = when { + expr.callee != null -> { + val instance = func.accept(this).asExpr(ctx.addressSort) + val type = scope.calcOnState { + scope.assert(memory.types.evalIsSubtype(instance, func.type)) ?: throw IllegalStateException() + memory.typeStreamOf(instance).first() + } + program.findMethod(expr.location, "(${type.typeName}).${expr.callee!!.name}") + } + + func is GoFunction -> program.findMethod(expr.location, func.metName) + func is GoVar -> scope.calcOnState { + program.findMethod(expr.location, (memory.read(URegisterStackLValue(ctx.addressSort, index(func.name))) as KConst).decl.name) + } + + else -> throw UnknownFunctionException(func.toString()) + } + if (method.blocks.isEmpty()) { + return mockCall(expr, method) + } + + val parameters = args.map { it.accept(this) }.toTypedArray() + val call = GoCall(method, applicationGraph.entryPoints(method).first()) + ctx.setMethodInfo(method, parameters) + + scope.doWithState { + addCall(call, currentStatement) + } + return ctx.noValue + } + + override fun visitGoAllocExpr(expr: GoAllocExpr): UExpr { + return mkPointer(expr.type) + } + + override fun visitGoPhiExpr(expr: GoPhiExpr): UExpr { + val currentBlock = expr.location.index + val lastBlock = scope.calcOnState { + var node = pathNode + while (node.statement.location.index == currentBlock) { + node = node.parent!! + } + node.statement.location.index + } + val block = expr.location.method.blocks[currentBlock] + + block.predecessors.forEachIndexed { i, pred -> + if (lastBlock != pred) { + return@forEachIndexed + } + + return expr.edges[i].accept(this) + } + return ctx.nullRef + } + + override fun visitGoAddExpr(expr: GoAddExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoSubExpr(expr: GoSubExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoMulExpr(expr: GoMulExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoDivExpr(expr: GoDivExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoModExpr(expr: GoModExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoAndExpr(expr: GoAndExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoOrExpr(expr: GoOrExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoXorExpr(expr: GoXorExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoShlExpr(expr: GoShlExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoShrExpr(expr: GoShrExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoAndNotExpr(expr: GoAndNotExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoEqlExpr(expr: GoEqlExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoNeqExpr(expr: GoNeqExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoLssExpr(expr: GoLssExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoLeqExpr(expr: GoLeqExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoGtrExpr(expr: GoGtrExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoGeqExpr(expr: GoGeqExpr): UExpr = visitGoBinaryExpr(expr) + + override fun visitGoUnNotExpr(expr: GoUnNotExpr): UExpr = visitGoUnaryExpr(expr) + + override fun visitGoUnSubExpr(expr: GoUnSubExpr): UExpr = visitGoUnaryExpr(expr) + + override fun visitGoUnArrowExpr(expr: GoUnArrowExpr): UExpr = visitGoUnaryExpr(expr) + + override fun visitGoUnMulExpr(expr: GoUnMulExpr): UExpr = visitGoUnaryExpr(expr) + + override fun visitGoUnXorExpr(expr: GoUnXorExpr): UExpr = visitGoUnaryExpr(expr) + + override fun visitGoChangeTypeExpr(expr: GoChangeTypeExpr): UExpr { + return changeType(expr.operand.accept(this), expr.operand.type, expr.type) + } + + override fun visitGoConvertExpr(expr: GoConvertExpr): UExpr { + val value = expr.operand.accept(this) + return when (val type = expr.type) { + GoBasicTypes.UNSAFE_POINTER -> ctx.mkBv(-1L) + GoBasicTypes.STRING -> { + val sliceType = expr.operand.type as SliceType + when (sliceType.elementType) { + GoBasicTypes.RUNE -> ctx.nullRef // TODO + GoBasicTypes.UINT8 -> value + else -> throw IllegalStateException("only []rune and []int can be converted to string") + } + } + + is PointerType -> mkPointer(type) + else -> { + when (expr.operand.type) { + GoBasicTypes.UNSAFE_POINTER -> tryBox(ctx.mkBv(-1L), expr.type) + GoBasicTypes.STRING -> { + val sliceType = type as SliceType + when (sliceType.elementType) { + GoBasicTypes.RUNE -> ctx.nullRef // TODO + GoBasicTypes.UINT8 -> value + else -> throw IllegalStateException("string can be converted only to []rune and []int") + } + } + + else -> ctx.mkPrimitiveCast(value, ctx.typeToSort(type)) + } + } + } + } + + override fun visitGoMultiConvertExpr(expr: GoMultiConvertExpr): UExpr { + // this is something about generics? + return unsupportedExpr("MultiConvert") + } + + override fun visitGoChangeInterfaceExpr(expr: GoChangeInterfaceExpr): UExpr { + return expr.operand.accept(this) + } + + override fun visitGoSliceToArrayPointerExpr(expr: GoSliceToArrayPointerExpr): UExpr { + val slice = unboxNamedRef(expr.operand.accept(this).asExpr(ctx.addressSort), expr.operand.type) + val sliceType = expr.operand.type.underlying() as SliceType + val sliceLength = scope.calcOnState { memory.readArrayLength(slice, sliceType, ctx.sizeSort) } + + val arrayType = (expr.type as PointerType).baseType.underlying() as ArrayType + val arrayLength = ctx.mkSizeExpr(arrayType.len.toInt()) + + checkLength(sliceLength) ?: throw IllegalStateException() + checkNotNull(slice) ?: throw IllegalStateException() + checkSliceToArrayPointerLength(sliceLength, arrayLength) ?: throw IllegalStateException() + + return scope.calcOnState { + val array = memory.allocateArray(arrayType, ctx.sizeSort, arrayLength) + for (i in 0 until arrayType.len) { + val idx = ctx.mkSizeExpr(i.toInt()) + val element = memory.readArrayIndex(slice, idx, sliceType, ctx.typeToSort(sliceType.elementType)) + memory.writeArrayIndex(array, idx, arrayType, ctx.typeToSort(arrayType.elementType), element, ctx.trueExpr) + } + + tryBox(mkPointer(arrayType, array), expr.type) + } + } + + override fun visitGoMakeInterfaceExpr(expr: GoMakeInterfaceExpr): UExpr { + val value = box(expr.value.accept(this@GoExprVisitor), expr.value.type) + scope.doWithState { + scope.assert(memory.types.evalIsSubtype(value, expr.type)) ?: throw IllegalStateException() + } + + return value + } + + override fun visitGoMakeClosureExpr(expr: GoMakeClosureExpr): UExpr { + return ctx.mkConst(expr.func.name, ctx.addressSort) + } + + override fun visitGoMakeMapExpr(expr: GoMakeMapExpr): UExpr { + val reserve = expr.reserve.accept(this).asExpr(ctx.sizeSort) + val mapType = expr.type.underlying() + + checkLength(reserve) ?: throw IllegalStateException() + + return scope.calcOnState { + val ref = memory.allocConcrete(mapType) + memory.write(UMapLengthLValue(ref, mapType, ctx.sizeSort), reserve, ctx.trueExpr) + tryBox(ref.asExpr(ctx.addressSort), expr.type) + } + } + + override fun visitGoMakeChanExpr(expr: GoMakeChanExpr): UExpr { + // channels aren't supported now + return unsupportedExpr("MakeChan") + } + + override fun visitGoMakeSliceExpr(expr: GoMakeSliceExpr): UExpr { + val len = expr.len.accept(this).asExpr(ctx.sizeSort) + + checkLength(len) ?: throw IllegalStateException() + + return scope.calcOnState { + tryBox(memory.allocateArray(expr.type.underlying(), ctx.sizeSort, len), expr.type) + } + } + + override fun visitGoSliceExpr(expr: GoSliceExpr): UExpr { + val boxedArray = expr.array.accept(this).asExpr(ctx.addressSort) + val array = unboxNamedRef(boxedArray, expr.array.type).let { if (isPointerConcrete(it)) deref(it, ctx.addressSort) else it } + val arrayType = expr.array.type.let { if (it is PointerType) it.baseType else it }.underlying() + val sliceType = expr.type.underlying() + val length = scope.calcOnState { memory.readArrayLength(array, arrayType, ctx.sizeSort) } + val low = expr.low.accept(this).asExpr(ctx.sizeSort) + val high = expr.high.accept(this).let { if (it is KBitVec32Value && it.intValue == -1) length else it }.asExpr(ctx.sizeSort) + val count = ctx.mkSizeSubExpr(high, low) + val elementType = when (arrayType) { + is ArrayType -> arrayType.elementType + is SliceType -> arrayType.elementType + GoBasicTypes.STRING -> GoBasicTypes.UINT8 + else -> throw IllegalStateException("illegal type for collection") + } + val elementSort = ctx.typeToSort(elementType) + + checkLength(length) ?: throw IllegalStateException() + checkNotNull(array) ?: throw IllegalStateException() + checkNegativeIndex(low) ?: throw IllegalStateException() + checkNegativeIndex(high) ?: throw IllegalStateException() + checkIndexOutOfBounds(low, ctx.mkSizeAddExpr(high, ctx.mkSizeExpr(1))) ?: throw IllegalStateException() + checkIndexOutOfBounds(high, ctx.mkSizeAddExpr(length, ctx.mkSizeExpr(1))) ?: throw IllegalStateException() + + return scope.calcOnState { + val result = memory.allocateArray(sliceType, ctx.sizeSort, count) + when (arrayType) { + is ArrayType -> { + val tempArray = memory.allocateArray(sliceType, ctx.sizeSort, count) + for (i in 0 until arrayType.len) { + val idx = ctx.mkSizeExpr(i.toInt()) + val element = memory.readArrayIndex(array, idx, arrayType, elementSort) + memory.writeArrayIndex(tempArray, idx, sliceType, elementSort, element, ctx.trueExpr) + } + memory.memcpy(tempArray, result, sliceType, elementSort, low, ctx.mkSizeExpr(0), count) + } + + else -> memory.memcpy(array, result, sliceType, elementSort, low, ctx.mkSizeExpr(0), count) + } + tryBox(result, expr.type) + } + } + + override fun visitGoFieldAddrExpr(expr: GoFieldAddrExpr): UExpr { + if (expr.instance is GoNullConstant) { + return scope.calcOnState { + panic("nil struct") + ctx.noValue + } + } + + val pointer = expr.instance.accept(this).asExpr(ctx.addressSort) + val struct = deref(pointer, ctx.addressSort) + checkNotNull(struct) ?: throw IllegalStateException() + + val fieldType = (expr.type as PointerType).baseType + val fieldLValue = UFieldLValue(ctx.typeToSort(fieldType), struct, expr.field) + return mkPointer(fieldType, fieldLValue) + } + + override fun visitGoFieldExpr(expr: GoFieldExpr): UExpr { + val struct = expr.instance.accept(this).asExpr(ctx.addressSort) + checkNotNull(struct) ?: throw IllegalStateException() + return scope.calcOnState { + memory.readField(struct, expr.field, ctx.typeToSort(expr.type)) + } + } + + override fun visitGoIndexAddrExpr(expr: GoIndexAddrExpr): UExpr { + val (array, index) = visitIndexExpr(expr.instance, expr.index) + val arrayType = expr.instance.type.let { if (it is PointerType) it.baseType else it }.underlying() + + val elementType = (expr.type as PointerType).baseType + val elementLValue = UArrayIndexLValue(ctx.typeToSort(elementType), array, index, arrayType) + return mkPointer(elementType, elementLValue) + } + + override fun visitGoIndexExpr(expr: GoIndexExpr): UExpr { + val (array, index) = visitIndexExpr(expr.instance, expr.index) + val arrayType = expr.instance.type.let { if (it is PointerType) it.baseType else it }.underlying() + return scope.calcOnState { + memory.readArrayIndex(array, index, arrayType, ctx.typeToSort(expr.type)) + } + } + + override fun visitGoLookupExpr(expr: GoLookupExpr): UExpr { + val map = unboxNamedRef(expr.instance.accept(this).asExpr(ctx.addressSort), expr.instance.type) + val mapType = expr.instance.type.underlying() as MapType + val key = expr.index.accept(this) + + val isRefKey = key.sort == ctx.addressSort + val commaOk = expr.commaOk + val valueSort = ctx.typeToSort(mapType.valueType) + + checkNotNull(map) ?: throw IllegalStateException() + scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw IllegalStateException() + + val contains = scope.calcOnState { + if (isRefKey) { + memory.refSetContainsElement(map, key.asExpr(ctx.addressSort), mapType) + } else { + memory.setContainsElement(map, key, mapType, USizeExprKeyInfo()) + } + } + val lvalue = if (isRefKey) { + URefMapEntryLValue(valueSort, map, key.asExpr(ctx.addressSort), mapType) + } else { + UMapEntryLValue(key.sort, valueSort, map, key.asExpr(key.sort), mapType, USizeExprKeyInfo()) + } + val rvalue = scope.calcOnState { memory.read(lvalue).asExpr(valueSort) } + + return scope.calcOnState { + if (commaOk) { + mkTuple(TupleType(listOf(mapType.valueType, GoBasicTypes.BOOL)), rvalue, contains) + } else { + ctx.mkIte(contains, { rvalue }, { rvalue.sort.sampleUValue() }) + } + } + } + + override fun visitGoSelectExpr(expr: GoSelectExpr): UExpr { + // channels aren't supported now + return unsupportedExpr("Select") + } + + override fun visitGoRangeExpr(expr: GoRangeExpr): UExpr { + val collection = expr.instance.accept(this).asExpr(ctx.addressSort).let { + when (val type = expr.instance.type.underlying()) { + is MapType -> copyMap(it, type) + GoBasicTypes.STRING -> it + else -> throw IllegalStateException("illegal type for range") + } + } + return scope.calcOnState { + mkTuple( + TupleType(listOf(expr.instance.type, GoBasicTypes.INT32)), + collection, ctx.mkSizeExpr(0) + ) + } + } + + override fun visitGoNextExpr(expr: GoNextExpr): UExpr { + val iter = expr.instance.accept(this).asExpr(ctx.addressSort) + return scope.calcOnState { + val tupleType = memory.typeStreamOf(iter).commonSuperType as TupleType + val collection = memory.readField(iter, 0, ctx.addressSort) + val notNull = ctx.mkNot(ctx.mkHeapRefEq(collection, ctx.nullRef)) + when (val collectionType = tupleType.types[0].underlying()) { + GoBasicTypes.STRING -> { + val index = memory.readField(iter, 1, ctx.sizeSort) + val char = memory.readArrayIndex(collection, index, collectionType, ctx.bv8Sort) + val length = memory.readArrayLength(collection, collectionType, ctx.sizeSort) + val ok = ctx.mkAnd(notNull, ctx.mkBvSignedLessExpr(index, length)) + + checkLength(length) ?: throw IllegalStateException() + + memory.writeField(iter, 1, ctx.sizeSort, ctx.mkBvAddExpr(index, ctx.mkSizeExpr(1)), ctx.trueExpr) + mkTuple( + TupleType(listOf(GoBasicTypes.BOOL, GoBasicTypes.INT32, GoBasicTypes.INT32)), + ok, index, ctx.mkPrimitiveCast(char, ctx.bv32Sort) + ) + } + + is MapType -> { + scope.ensureObjectMapSizeCorrect(collection, collectionType) ?: throw IllegalStateException() + + val length = symbolicObjectMapSize(collection, collectionType) + val ok = ctx.mkAnd(notNull, ctx.mkBvSignedGreaterExpr(length, ctx.mkBv(0))) + val isPrimitiveKey = collectionType.keyType.underlying() is BasicType + val (key, value) = if (isPrimitiveKey) { + val k = symbolicPrimitiveMapAnyKey(collection, collectionType, ctx.typeToSort(collectionType.keyType), USizeExprKeyInfo()) + val v = symbolicPrimitiveMapGet(collection, k, collectionType, ctx.typeToSort(collectionType.valueType), USizeExprKeyInfo()) + symbolicPrimitiveMapRemove(collection, k, collectionType, USizeExprKeyInfo()) + k to v + } else { + val k = symbolicObjectMapAnyKey(collection, collectionType) + val v = symbolicObjectMapGet(collection, k, collectionType, ctx.typeToSort(collectionType.valueType)) + symbolicObjectMapRemove(collection, k, collectionType) + k to v + } + + mkTuple( + TupleType(listOf(GoBasicTypes.BOOL, collectionType.keyType, collectionType.valueType)), + ok, key, value + ) + } + + else -> throw IllegalStateException("invalid collection type in next expr") + } + } + } + + override fun visitGoTypeAssertExpr(expr: GoTypeAssertExpr): UExpr { + val x = expr.instance.accept(this) + + val assertType = expr.assertType.let { if (it is TupleType) it.types[0] else it } + val assertSort = ctx.typeToSort(assertType) + + val commaOk = expr.type is TupleType + val tupleType = TupleType(listOf(assertType, GoBasicTypes.BOOL)) + + val ite: (UHeapRef, UExpr, UExpr) -> UExpr = { ref, ok, fail -> + scope.calcOnState { + return@calcOnState ctx.mkIte( + memory.types.evalIsSupertype(ref, assertType), + trueBranch = { ok.asExpr(ok.sort) }, + falseBranch = { fail.asExpr(fail.sort).also { if (!commaOk) panic("type assertion failed") } } + ) + } + } + + val xAddr = x.asExpr(ctx.addressSort) + checkNotNull(xAddr) ?: throw IllegalStateException() + val unboxedValue = unbox(xAddr, assertSort) + + return scope.calcOnState { + val sample = if (assertSort == ctx.addressSort) ctx.nullRef else assertSort.sampleUValue().asExpr(assertSort) + if (commaOk) { + mkTuple(tupleType, ite(xAddr, unboxedValue, sample), ite(xAddr, ctx.trueExpr, ctx.falseExpr)) + } else { + ite(xAddr, unboxedValue, sample) + } + } + } + + override fun visitGoExtractExpr(expr: GoExtractExpr): UExpr { + val tuple = expr.instance.accept(this).asExpr(ctx.addressSort) + + return scope.calcOnState { + memory.readField(tuple, expr.index, ctx.typeToSort(expr.type)) + } + } + + override fun visitGoVar(expr: GoVar): UExpr { + return scope.calcOnState { + memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), index(expr.name))) + } + } + + override fun visitGoFreeVar(expr: GoFreeVar): UExpr { + return scope.calcOnState { + memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), expr.index + ctx.freeVariableOffset(lastEnteredMethod))) + } + } + + override fun visitGoParameter(expr: GoParameter): UExpr { + return scope.calcOnState { + memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), expr.index)) + } + } + + override fun visitGoConst(expr: GoConst): UExpr { + // const can't be visited + return unsupportedExpr("Const") + } + + override fun visitGoGlobal(expr: GoGlobal): UExpr { + return scope.calcOnState { + ctx.getGlobal(expr) + } + } + + override fun visitGoBuiltin(expr: GoBuiltin): UExpr { + // builtin can't be stored in a variable + return unsupportedExpr("Builtin") + } + + override fun visitGoFunction(expr: GoFunction): UExpr { + return ctx.mkConst(expr.metName, ctx.addressSort) + } + + override fun visitGoBool(value: GoBool): UExpr = with(ctx) { + return tryBox(mkBool(value.value), value.type) + } + + override fun visitGoInt(value: GoInt): UExpr = with(ctx) { + return tryBox(mkBv(value.value.toInt()), value.type) + } + + override fun visitGoInt8(value: GoInt8): UExpr = with(ctx) { + return tryBox(mkBv(value.value), value.type) + } + + override fun visitGoInt16(value: GoInt16): UExpr = with(ctx) { + return tryBox(mkBv(value.value), value.type) + } + + override fun visitGoInt32(value: GoInt32): UExpr = with(ctx) { + return tryBox(mkBv(value.value), value.type) + } + + override fun visitGoInt64(value: GoInt64): UExpr = with(ctx) { + return tryBox(mkBv(value.value), value.type) + } + + override fun visitGoUInt(value: GoUInt): UExpr = with(ctx) { + return tryBox(mkBv(value.value.toInt()), value.type) + } + + override fun visitGoUInt8(value: GoUInt8): UExpr = with(ctx) { + return tryBox(mkBv(value.value.toByte()), value.type) + } + + override fun visitGoUInt16(value: GoUInt16): UExpr = with(ctx) { + return tryBox(mkBv(value.value.toShort()), value.type) + } + + override fun visitGoUInt32(value: GoUInt32): UExpr = with(ctx) { + return tryBox(mkBv(value.value.toInt()), value.type) + } + + override fun visitGoUInt64(value: GoUInt64): UExpr = with(ctx) { + return tryBox(mkBv(value.value.toLong()), value.type) + } + + override fun visitGoFloat32(value: GoFloat32): UExpr = with(ctx) { + return tryBox(mkFp(value.value, fp32Sort), value.type) + } + + override fun visitGoFloat64(value: GoFloat64): UExpr = with(ctx) { + return tryBox(mkFp(value.value, fp64Sort), value.type) + } + + override fun visitGoNullConstant(value: GoNullConstant): UExpr = ctx.nullRef + + override fun visitGoStringConstant(value: GoStringConstant): UExpr { + return scope.calcOnState { + tryBox(mkString(value.value), value.type) + } + } + + fun checkNotNull(obj: UHeapRef): Unit? = with(ctx) { + scope.fork(mkHeapRefEq(obj, nullRef).not(), blockOnFalseState = { + panic("null") + }) + } + + fun unboxNamedRef(expr: UHeapRef, type: GoType): UHeapRef { + if (type !is NamedType) { + return expr + } + + return unbox(expr, ctx.typeToSort(type.underlying())).asExpr(ctx.addressSort) + } + + private fun visitGoBinaryExpr(expr: GoBinaryExpr): UExpr { + if (expr.lhv.type.underlying() == GoBasicTypes.STRING && expr.rhv.type.underlying() == GoBasicTypes.STRING) { + return tryBox(appendArray(expr.lhv, expr.rhv), expr.type) + } + + val lhv = expr.lhv.accept(this) + val rhv = expr.rhv.accept(this) + + val signed = expr.lhv.type.underlying() is BasicType && !expr.lhv.type.underlying().typeName.startsWith("ui") + val lhs = unboxNamedPrimitive(lhv, expr.lhv.type) + val rhs = unboxNamedPrimitive(rhv, expr.rhv.type) + val result = when (expr) { + is GoAddExpr -> GoBinaryOperator.Add + is GoSubExpr -> GoBinaryOperator.Sub + is GoMulExpr -> GoBinaryOperator.Mul + is GoDivExpr -> GoBinaryOperator.Div(signed) + is GoModExpr -> GoBinaryOperator.Mod(signed) + is GoAndExpr -> GoBinaryOperator.And + is GoOrExpr -> GoBinaryOperator.Or + is GoXorExpr -> GoBinaryOperator.Xor + is GoShlExpr -> GoBinaryOperator.Shl + is GoShrExpr -> GoBinaryOperator.Shr(signed) + is GoAndNotExpr -> GoBinaryOperator.AndNot + is GoEqlExpr -> GoBinaryOperator.Eql + is GoLssExpr -> GoBinaryOperator.Lss(signed) + is GoGtrExpr -> GoBinaryOperator.Gtr(signed) + is GoNeqExpr -> GoBinaryOperator.Neq + is GoLeqExpr -> GoBinaryOperator.Leq(signed) + is GoGeqExpr -> GoBinaryOperator.Geq(signed) + else -> throw UnknownBinaryOperationException(expr.toString()) + }(lhs, normalize(lhs, rhs, expr, signed)) + + if (expr.type is NamedType) { + return box(result, expr.type) + } + return result + } + + private fun visitGoUnaryExpr(expr: GoUnaryExpr): UExpr { + val x = expr.value.accept(this) + return when (expr) { + is GoUnArrowExpr -> throw UnsupportedUnaryOperationException("channel operations") + is GoUnXorExpr, is GoUnNotExpr, is GoUnSubExpr -> GoUnaryOperator.Neg(x) + is GoUnMulExpr -> deref(x, ctx.typeToSort(expr.type)) + else -> throw UnknownUnaryOperationException(expr.toString()) + } + } + + private fun visitIndexExpr(instance: GoValue, idx: GoValue): Pair> { + val array = unboxNamedRef(instance.accept(this).asExpr(ctx.addressSort), instance.type).let { if (isPointerConcrete(it)) deref(it, ctx.addressSort) else it } + val arrayType = instance.type.let { if (it is PointerType) it.baseType else it }.underlying() + val index = bv(unboxNamedPrimitive(idx.accept(this), idx.type)).mkNarrow(Int.SIZE_BITS, false).asExpr(ctx.sizeSort) + val length = scope.calcOnState { memory.readArrayLength(array, arrayType, ctx.sizeSort) } + + checkLength(length) ?: throw IllegalStateException() + checkNotNull(array) ?: throw IllegalStateException() + checkNegativeIndex(index) ?: throw IllegalStateException() + checkIndexOutOfBounds(index, length) ?: throw IllegalStateException() + + return array to index + } + + private fun normalize(lhs: UExpr, rhs: UExpr, goExpr: GoBinaryExpr, signed: Boolean): UExpr = with(ctx) { + when (goExpr) { + is GoShrExpr -> if (bv(lhs).sort.sizeBits != bv(rhs).sort.sizeBits) bv(rhs).mkNarrow(Long.SIZE_BITS, signed).asExpr(bv64Sort) else rhs + else -> rhs + } + } + + private fun bv(expr: UExpr): UExpr { + return expr.asExpr(expr.sort as UBvSort) + } + + private fun isPointer(pointer: UHeapRef): UExpr { + return scope.calcOnState { isPointer(pointer) } + } + + private fun isPointerConcrete(pointer: UHeapRef): Boolean { + if (pointer is UNullRef) { + return false + } + return scope.calcOnState { + val index = 1 + val field = memory.readField(pointer, index, ctx.bv32Sort) + field is KBitVec32Value && field.intValue == POINTER_FIELD + } + } + + private fun isBoxed(ref: UHeapRef): UExpr { + return scope.calcOnState { isBoxed(ref) } + } + + private fun mkPointer(type: GoType): UConcreteHeapRef { + return scope.calcOnState { mkPointer(type) } + } + + private fun mkPointer(type: GoType, lvalue: ULValue<*, *>): UExpr { + return scope.calcOnState { mkPointer(type, lvalue) } + } + + private fun deref(expr: UExpr, sort: Sort): UExpr = with(ctx) { + val pointer = expr.asExpr(addressSort) + checkIsPointer(pointer) ?: throw IllegalStateException() + checkNotNull(pointer) ?: throw IllegalStateException() + return scope.calcOnState { + deref(pointer, sort).asExpr(sort) + } + } + + private fun box(expr: UExpr, targetType: GoType): UHeapRef { + return scope.calcOnState { + box(expr, targetType) + } + } + + private fun unbox(expr: UHeapRef, sort: USort): UExpr { + checkIsBoxed(expr) ?: throw IllegalStateException() + checkNotNull(expr) ?: throw IllegalStateException() + return scope.calcOnState { + unbox(expr, sort) + } + } + + private fun unboxNamedPrimitive(expr: UExpr, type: GoType): UExpr { + if (type !is NamedType) { + return expr + } + + return unbox(expr.asExpr(ctx.addressSort), ctx.typeToSort(type.underlying())) + } + + private fun tryBox(expr: UExpr, targetType: GoType): UExpr { + return if (targetType is NamedType) box(expr, targetType) else expr + } + + private fun index(name: String): Int { + return name.substring(1).toInt() + ctx.localVariableOffset(scope.calcOnState { lastEnteredMethod }) + } + + private fun checkIndexOutOfBounds(index: UExpr, length: UExpr): Unit? = with(ctx) { + scope.fork(mkSizeLtExpr(index, length), blockOnFalseState = { + panic("index out of bounds") + }) + } + + private fun checkNegativeIndex(value: UExpr): Unit? = with(ctx) { + scope.fork(mkSizeGeExpr(value, mkSizeExpr(0)), blockOnFalseState = { + panic("negative index") + }) + } + + private fun checkLength(length: UExpr): Unit? = with(ctx) { + scope.assert(mkSizeGeExpr(length, mkSizeExpr(0))) + } + + private fun checkSliceToArrayPointerLength( + sliceLength: UExpr, + arrayLength: UExpr + ): Unit? = with(ctx) { + scope.fork(mkSizeGeExpr(sliceLength, arrayLength), blockOnFalseState = { + panic("length of the slice is less than the length of the array") + }) + } + + private fun checkIsPointer(obj: UHeapRef): Unit? = scope.fork(isPointer(obj), blockOnFalseState = { + panic("not a pointer") + }) + + private fun checkIsBoxed(obj: UHeapRef): Unit? = scope.fork(isBoxed(obj), blockOnFalseState = { + panic("not a boxed value") + }) + + private fun callBuiltin(method: GoBuiltin, args: List, returnType: GoType): UExpr { + return when (method.name) { + "append" -> tryBox(appendArray(args[0], args[1]), returnType) + "copy" -> { + val src = unboxNamedRef(args[1].accept(this).asExpr(ctx.addressSort), args[1].type) + val dst = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) + val sliceType = args[0].type.underlying() as SliceType + val elementSort = ctx.typeToSort(sliceType.elementType) + + return scope.calcOnState { + val srcLength = memory.readArrayLength(src, sliceType, ctx.sizeSort) + val dstLength = memory.readArrayLength(dst, sliceType, ctx.sizeSort) + checkLength(srcLength) ?: throw IllegalStateException() + checkLength(dstLength) ?: throw IllegalStateException() + val numCopied = ctx.mkIte(ctx.mkSizeLtExpr(srcLength, dstLength), srcLength, dstLength) + + memory.memcpy(src, dst, sliceType, elementSort, ctx.mkSizeExpr(0), ctx.mkSizeExpr(0), numCopied) + numCopied + } + } + + "delete" -> { + val map = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) + val key = args[1].accept(this) + val mapType = args[0].type.underlying() as MapType + val keySort = ctx.typeToSort(mapType.keyType) + + scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw IllegalStateException() + scope.doWithState { + if (keySort == ctx.addressSort) { + symbolicObjectMapRemove(map, key.asExpr(ctx.addressSort), mapType) + } else { + symbolicPrimitiveMapRemove(map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) + } + } + return ctx.voidValue + } + + "len", "cap" -> { + val arg = args[0] + val collection = unboxNamedRef(arg.accept(this).asExpr(ctx.addressSort), arg.type) + + return ctx.mkIte( + ctx.mkNot(ctx.mkHeapRefEq(collection, ctx.nullRef)), + trueBranch = { + scope.calcOnState { + when (val type = arg.type.underlying()) { + is ArrayType, is SliceType, is BasicType -> { + val length = memory.readArrayLength(collection, type, ctx.sizeSort) + checkLength(length) ?: throw IllegalStateException() + length + } + + is MapType -> { + scope.ensureObjectMapSizeCorrect(collection, type) ?: throw IllegalStateException() + symbolicObjectMapSize(collection, type) + } + + else -> throw IllegalStateException() + } + } + }, + falseBranch = { + ctx.mkSizeExpr(0) + }, + ) + } + + "panic" -> { + val value = args[0].accept(this) + scope.calcOnState { + panic(value, args[0].type) + ctx.noValue + } + } + + "recover" -> scope.calcOnState { + recover() + } + + else -> ctx.nullRef + } + } + + private fun copyMap( + srcMap: UHeapRef, + mapType: MapType, + ): UHeapRef = with(ctx) { + checkNotNull(srcMap) ?: throw IllegalStateException() + scope.ensureObjectMapSizeCorrect(srcMap, mapType) ?: throw IllegalStateException() + + val keySort = typeToSort(mapType.keyType) + val valueSort = typeToSort(mapType.valueType) + val isRefSet = keySort == addressSort + return scope.calcOnState { + val destMap = mkSymbolicObjectMap(mapType) + if (isRefSet) { + symbolicObjectMapMergeInto(destMap, srcMap, mapType, valueSort) + } else { + symbolicPrimitiveMapMergeInto(destMap, srcMap, mapType, keySort, valueSort, USizeExprKeyInfo()) + } + destMap + } + } + + private fun appendArray(sliceValue: GoValue, appendValue: GoValue): UExpr { + val slice = unboxNamedRef(sliceValue.accept(this).asExpr(ctx.addressSort), sliceValue.type) + val append = unboxNamedRef(appendValue.accept(this).asExpr(ctx.addressSort), appendValue.type) + val sliceType = sliceValue.type.underlying() + + val elementSort = if (sliceType == GoBasicTypes.STRING) ctx.bv8Sort else ctx.typeToSort((sliceType as SliceType).elementType) + val zero = ctx.mkSizeExpr(0) + + return scope.calcOnState { + val sliceLength = ctx.mkIte( + ctx.mkHeapRefEq(slice, ctx.nullRef), + { zero }, + { memory.readArrayLength(slice, sliceType, ctx.sizeSort) } + ) + val appendLength = ctx.mkIte( + ctx.mkHeapRefEq(append, ctx.nullRef), + { zero }, + { memory.readArrayLength(append, sliceType, ctx.sizeSort) } + ) + checkLength(sliceLength) ?: throw IllegalStateException() + checkLength(appendLength) ?: throw IllegalStateException() + + val length = ctx.mkSizeAddExpr(sliceLength, appendLength) + val out = memory.allocateArray(sliceType, ctx.sizeSort, length) + + memory.memcpy(slice, out, sliceType, elementSort, zero, zero, sliceLength) + memory.memcpy(append, out, sliceType, elementSort, zero, sliceLength, appendLength) + + out + } + } + + private fun changeType(value: UExpr, baseType: GoType, targetType: GoType): UExpr { + return when (targetType) { + is NamedType -> { + when (baseType) { + is NamedType -> box(unbox(value.asExpr(ctx.addressSort), ctx.typeToSort(baseType)), targetType) + else -> box(value, targetType) + } + } + + is PointerType -> { + ctx.mkIte( + ctx.mkHeapRefEq(value.asExpr(ctx.addressSort), ctx.nullRef), + trueBranch = { ctx.nullRef }, + falseBranch = { + val basePointerType = (baseType as PointerType).baseType + val baseValue = deref(value.asExpr(ctx.addressSort), ctx.typeToSort(basePointerType)) + val targetPointerType = targetType.baseType + val targetValue = changeType(baseValue, basePointerType, targetPointerType) + scope.calcOnState { mkPointer(targetPointerType, targetValue) } + } + ) + } + + else -> { + unbox(value.asExpr(ctx.addressSort), ctx.typeToSort(targetType)) + } + } + } + + private fun mockCall(expr: GoCallExpr, func: GoValue): UExpr { + val funcName = when(func) { + is GoParameter -> func.name + is GoFunction -> func.name + else -> "unnamed" + } + val signature = func.type as SignatureType + val returnType = when (signature.results.types.size) { + 0 -> NullType() + 1 -> signature.results.types[0] + else -> signature.results + } + val method = GoFunction(signature, emptyList(), funcName, emptyList(), "", emptyList(), emptyList()) + val mockSort = ctx.typeToSort(returnType) + val mockValue = scope.calcOnState { + memory.mocker.call(method, expr.args.map { it.accept(this@GoExprVisitor) }.asSequence(), mockSort, memory.ownership) + } + if (mockSort == ctx.addressSort) { + val constraint = scope.calcOnState { + memory.types.evalIsSubtype(mockValue.asExpr(ctx.addressSort), returnType) + } + scope.assert(constraint) + } + return mockValue + } + + private fun unsupportedExpr(name: String): UExpr { + throw UnsupportedOperationException("Expression '$name' not supported") + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt new file mode 100644 index 0000000000..81f8391f93 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -0,0 +1,203 @@ +package org.usvm + +import io.ksmt.expr.KConst +import io.ksmt.utils.asExpr +import org.jacodb.go.api.GoAssignInst +import org.jacodb.go.api.GoCallInst +import org.jacodb.go.api.GoDebugRefInst +import org.jacodb.go.api.GoDeferInst +import org.jacodb.go.api.GoGoInst +import org.jacodb.go.api.GoIfInst +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoInstVisitor +import org.jacodb.go.api.GoJumpInst +import org.jacodb.go.api.GoMapUpdateInst +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoNullInst +import org.jacodb.go.api.GoPanicInst +import org.jacodb.go.api.GoReturnInst +import org.jacodb.go.api.GoRunDefersInst +import org.jacodb.go.api.GoSendInst +import org.jacodb.go.api.GoStoreInst +import org.jacodb.go.api.GoVar +import org.jacodb.go.api.NullType +import org.jacodb.go.api.TupleType +import org.usvm.api.collection.ObjectMapCollectionApi.ensureObjectMapSizeCorrect +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapSize +import org.usvm.collection.map.length.UMapLengthLValue +import org.usvm.collection.map.primitive.UMapEntryLValue +import org.usvm.collection.map.ref.URefMapEntryLValue +import org.usvm.collection.set.primitive.USetEntryLValue +import org.usvm.collection.set.ref.URefSetEntryLValue +import org.usvm.interpreter.GoStepScope +import org.usvm.memory.URegisterStackLValue +import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.statistics.ApplicationGraph +import org.usvm.type.underlying + +class GoInstVisitor( + private val ctx: GoContext, + private val program: GoProgram, + private val scope: GoStepScope, + private val exprVisitor: GoExprVisitor, + private val applicationGraph: ApplicationGraph, +) : GoInstVisitor { + override fun visitGoJumpInst(inst: GoJumpInst): GoInst { + return inst.location.method.blocks[inst.target.index].instructions[0] + } + + override fun visitGoIfInst(inst: GoIfInst): GoInst = with(ctx) { + val pos = inst.location.method.blocks[inst.trueBranch.index].instructions[0] + val neg = inst.location.method.blocks[inst.falseBranch.index].instructions[0] + + scope.forkWithBlackList( + inst.condition.accept(exprVisitor).asExpr(boolSort), + pos, + neg, + blockOnTrueState = { newInst(pos) }, + blockOnFalseState = { newInst(neg) } + ) + GoNullInst(inst.location.method) + } + + override fun visitGoReturnInst(inst: GoReturnInst): GoInst { + scope.doWithState { + when (inst.returnValues.size) { + 0 -> returnValue(ctx.voidValue, NullType()) + 1 -> returnValue(inst.returnValues[0].accept(exprVisitor), inst.returnValues[0].type) + else -> { + val type = TupleType(inst.returnValues.map { it.type }) + returnValue(mkTuple(type, fields = inst.returnValues.map { it.accept(exprVisitor) }.toTypedArray()), type) + } + } + } + return GoNullInst(inst.location.method) + } + + override fun visitGoRunDefersInst(inst: GoRunDefersInst): GoInst { + return scope.calcOnState { + runDefers() + next(inst) + } + } + + override fun visitGoPanicInst(inst: GoPanicInst): GoInst { + val value = inst.throwable.accept(exprVisitor) + + return scope.calcOnState { + panic(value, inst.throwable.type) + next(inst) + } + } + + override fun visitGoGoInst(inst: GoGoInst): GoInst { + return unsupportedInst("Go") + } + + override fun visitGoDeferInst(inst: GoDeferInst): GoInst { + val name = (inst.func.accept(exprVisitor) as KConst).toString() + val method = program.findMethod(inst.location, name) + + val parameters = inst.args.map { it.accept(exprVisitor) }.toTypedArray() + val call = GoCall(method, applicationGraph.entryPoints(method).first()) + ctx.setMethodInfo(method, parameters) + + scope.doWithState { + data.addDeferredCall(lastEnteredMethod, call) + } + return next(inst) + } + + override fun visitGoSendInst(inst: GoSendInst): GoInst { + return unsupportedInst("Send") + } + + override fun visitGoStoreInst(inst: GoStoreInst): GoInst { + val pointer = inst.lhv.accept(exprVisitor).asExpr(ctx.addressSort) + if (pointer is UNullRef) { + return scope.calcOnState { + panic("null pointer dereference") + next(inst) + } + } + val rvalue = inst.rhv.accept(exprVisitor) + scope.doWithState { + store(pointer, rvalue) + } + + return next(inst) + } + + override fun visitGoMapUpdateInst(inst: GoMapUpdateInst): GoInst { + val map = exprVisitor.unboxNamedRef(inst.map.accept(exprVisitor).asExpr(ctx.addressSort), inst.map.type) + val type = inst.map.type.underlying() + val key = inst.key.accept(exprVisitor) + val value = inst.value.accept(exprVisitor) + val isRefKey = key.sort == ctx.addressSort + + exprVisitor.checkNotNull(map) ?: throw IllegalStateException() + scope.ensureObjectMapSizeCorrect(map, type) ?: throw IllegalStateException() + + scope.doWithState { + val mapContainsLValue = if (isRefKey) { + URefSetEntryLValue(map, key.asExpr(ctx.addressSort), type) + } else { + USetEntryLValue(key.sort, map, key.asExpr(key.sort), type, USizeExprKeyInfo()) + } + val mapEntryLValue = if (isRefKey) { + URefMapEntryLValue(value.sort, map, key.asExpr(ctx.addressSort), type) + } else { + UMapEntryLValue(key.sort, value.sort, map, key.asExpr(key.sort), type, USizeExprKeyInfo()) + } + val currentSize = symbolicObjectMapSize(map, type) + + memory.write(mapEntryLValue, value.asExpr(value.sort), ctx.trueExpr) + memory.write(mapContainsLValue, ctx.trueExpr, ctx.trueExpr) + + val updatedSize = ctx.mkSizeAddExpr(currentSize, ctx.mkSizeExpr(1)) + memory.write(UMapLengthLValue(map, type, ctx.sizeSort), updatedSize, ctx.mkNot(memory.read(mapContainsLValue))) + } + + return next(inst) + } + + override fun visitGoDebugRefInst(inst: GoDebugRefInst): GoInst { + return unsupportedInst("DebugRef") + } + + override fun visitExternalGoInst(inst: GoInst): GoInst { + return unsupportedInst("External") + } + + override fun visitGoAssignInst(inst: GoAssignInst): GoInst { + val index = index((inst.lhv as GoVar).name) + val rvalue = inst.rhv.accept(exprVisitor) + val sort = rvalue.sort + + if (rvalue == ctx.noValue) { + return GoNullInst(inst.location.method) + } + + scope.doWithState { + memory.write(URegisterStackLValue(sort, index), rvalue.asExpr(sort), ctx.trueExpr) + } + + return next(inst) + } + + override fun visitGoCallInst(inst: GoCallInst): GoInst { + return unsupportedInst("Call") + } + + private fun next(inst: GoInst): GoInst { + return applicationGraph.successors(inst).ifEmpty { sequenceOf(GoNullInst(inst.location.method)) }.first() + } + + private fun unsupportedInst(name: String): GoInst { + throw UnsupportedOperationException("Instruction '$name' not supported") + } + + private fun index(name: String): Int { + return name.substring(1).toInt() + ctx.localVariableOffset(scope.calcOnState { lastEnteredMethod }) + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt new file mode 100644 index 0000000000..9fbdc7e51a --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt @@ -0,0 +1,172 @@ +package org.usvm + +import mu.KLogging +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoMethod +import org.usvm.interpreter.GoInterpreter +import org.usvm.interpreter.GoTestInterpreter +import org.usvm.interpreter.ProgramExecutionResult +import org.usvm.ps.createPathSelector +import org.usvm.state.GoMethodResult +import org.usvm.state.GoState +import org.usvm.statistics.CompositeUMachineObserver +import org.usvm.statistics.CoverageStatistics +import org.usvm.statistics.StatisticsByMethodPrinter +import org.usvm.statistics.StepsStatistics +import org.usvm.statistics.TimeStatistics +import org.usvm.statistics.TransitiveCoverageZoneObserver +import org.usvm.statistics.UMachineObserver +import org.usvm.statistics.collectors.AllStatesCollector +import org.usvm.statistics.collectors.CoveredNewStatesCollector +import org.usvm.statistics.collectors.TargetsReachedStatesCollector +import org.usvm.statistics.constraints.SoftConstraintsObserver +import org.usvm.statistics.distances.CallGraphStatisticsImpl +import org.usvm.statistics.distances.CfgStatisticsImpl +import org.usvm.statistics.distances.PlainCallGraphStatistics +import org.usvm.stopstrategies.createStopStrategy +import org.usvm.type.GoTypeSystem +import org.usvm.util.hasUnsupportedInstructions +import org.usvm.util.isInit +import kotlin.math.roundToInt + +internal typealias USizeSort = UBv32Sort + +val logger = object : KLogging() {}.logger + +class GoMachine( + private val program: GoProgram, + private val options: UMachineOptions, + private val customOptions: GoMachineOptions +) : UMachine() { + private val typeSystem = GoTypeSystem(options.typeOperationsTimeout, program.types.values) + private val applicationGraph = GoApplicationGraph() + private val components = GoComponents(typeSystem, options) + private val ctx = GoContext(components) + private val interpreter = GoInterpreter(ctx, program, applicationGraph) + private val cfgStatistics = CfgStatisticsImpl(applicationGraph) + private val testInterpreter = GoTestInterpreter(ctx) + + override fun close() { + ctx.close() + } + + fun analyzeAndResolve(pkg: GoPackage, methodName: String): Collection { + return analyzeAndResolve(pkg.findMethod(methodName)) + } + + fun analyzeAndResolve(method: GoMethod): Collection { + return analyze(method).map { testInterpreter.resolve(it, method) } + } + + private fun analyze(method: GoMethod): List { + return analyze(listOf(method)) + } + + private fun analyze(methodsList: List, targets: List = emptyList()): List { + logger.debug("{}.analyze()", this) + val excludedMethods = methodsList.filter { it.hasUnsupportedInstructions() }.map { it.metName } + if (excludedMethods.isNotEmpty()) { + logger.warn("The following methods will be skipped due to having unsupported instructions: {}", excludedMethods) + } + + val methods = methodsList.filter { !it.hasUnsupportedInstructions() } + val initialStates = hashMapOf() + methods.forEach { + initialStates[it] = interpreter.getInitialState(it, targets) + } + val timeStatistics = TimeStatistics() + val coverageStatistics = CoverageStatistics(methods.toSet(), applicationGraph) + val callGraphStatistics = + when (options.targetSearchDepth) { + 0u -> PlainCallGraphStatistics() + else -> CallGraphStatisticsImpl( + options.targetSearchDepth, + applicationGraph + ) + } + + val pathSelector = createPathSelector( + initialStates, + options, + applicationGraph, + timeStatistics, + { coverageStatistics }, + { cfgStatistics }, + { callGraphStatistics } + ) + val statesCollector = + when (options.stateCollectionStrategy) { + StateCollectionStrategy.COVERED_NEW -> CoveredNewStatesCollector(coverageStatistics) { + it.methodResult is GoMethodResult.Panic + } + + StateCollectionStrategy.REACHED_TARGET -> TargetsReachedStatesCollector() + StateCollectionStrategy.ALL -> AllStatesCollector() + } + val stepsStatistics = StepsStatistics() + val stopStrategy = createStopStrategy( + options, + targets, + timeStatisticsFactory = { timeStatistics }, + stepsStatisticsFactory = { stepsStatistics }, + coverageStatisticsFactory = { coverageStatistics }, + getCollectedStatesCount = { statesCollector.collectedStates.size } + ) + + val observers = mutableListOf>(coverageStatistics) + observers.add(statesCollector) + observers.add(timeStatistics) + observers.add(stepsStatistics) + if (options.coverageZone != CoverageZone.METHOD) { + observers.add( + TransitiveCoverageZoneObserver( + initialMethods = methods, + methodExtractor = { state -> state.currentStatement.location.method }, + addCoverageZone = { coverageStatistics.addCoverageZone(it) }, + ignoreMethod = { it.isInit() || it.hasUnsupportedInstructions() } + ) + ) + } + if (options.useSoftConstraints) { + observers.add(SoftConstraintsObserver()) + } + if (logger.isInfoEnabled) { + observers.add( + StatisticsByMethodPrinter( + { methods }, + logger::info, + { it.toString() }, + coverageStatistics, + timeStatistics, + stepsStatistics + ) + ) + } + + run( + interpreter, + pathSelector, + observer = CompositeUMachineObserver(observers), + isStateTerminated = ::isStateTerminated, + stopStrategy = stopStrategy, + ) + + println("Total coverage: ${coverageStatistics.getTotalCoverage().roundToInt()}%") + for (method in coverageStatistics.coverageZone) { + println("Method ${method.metName} coverage: ${coverageStatistics.getMethodCoverage(method).roundToInt()}%") + } + if (coverageStatistics.getTotalCoverage().roundToInt() < 100 && !customOptions.uncoveredMethods.containsAll(methods.map { it.metName })) { + if (customOptions.failOnNotFullCoverage) { + throw IllegalStateException("coverage not 100%") + } else { + logger.warn("analysis of methods ${methods.map { it.metName }} lead to coverage < 100%") + } + } + + return statesCollector.collectedStates + } + + private fun isStateTerminated(state: GoState): Boolean { + return state.callStack.isEmpty() + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt b/usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt new file mode 100644 index 0000000000..4f2c4349eb --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt @@ -0,0 +1,6 @@ +package org.usvm + +data class GoMachineOptions( + val failOnNotFullCoverage: Boolean, + val uncoveredMethods: List +) \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt b/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt new file mode 100644 index 0000000000..7f7e64f65a --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt @@ -0,0 +1,8 @@ +package org.usvm + +import org.jacodb.go.api.GoInst +import org.usvm.targets.UTarget + +abstract class GoTarget( + location: GoInst, +) : UTarget(location) \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/api/Exception.kt b/usvm-go/src/main/kotlin/org/usvm/api/Exception.kt new file mode 100644 index 0000000000..f32f7af147 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/api/Exception.kt @@ -0,0 +1,10 @@ +package org.usvm.api + +class UnknownMethodException(name: String) : Exception("Unknown method $name") +class UnknownPackageException(name: String) : Exception("Unknown package $name") +class UnknownUnaryOperationException(name: String) : Exception("Unknown unary operation: $name") +class UnknownBinaryOperationException(name: String) : Exception("Unknown binary operation: $name") +class UnknownFunctionException(name: String) : Exception("Unknown function $name") + +class UnsupportedUnaryOperationException(name: String) : Exception("Unsupported unary operation: $name") +class UnsupportedInstructionException(name: String) : Exception("Unsupported instruction: $name") diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt new file mode 100644 index 0000000000..302d41531e --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt @@ -0,0 +1,128 @@ +package org.usvm.interpreter + +import mu.KLogging +import org.jacodb.go.api.GoFunction +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoNullInst +import org.jacodb.go.api.GoType +import org.jacodb.go.api.PointerType +import org.usvm.GoCall +import org.usvm.GoContext +import org.usvm.GoExprVisitor +import org.usvm.GoInstVisitor +import org.usvm.GoProgram +import org.usvm.GoTarget +import org.usvm.NULL_ADDRESS +import org.usvm.StepResult +import org.usvm.StepScope +import org.usvm.UInterpreter +import org.usvm.collections.immutable.internal.MutabilityOwnership +import org.usvm.forkblacklists.UForkBlackList +import org.usvm.solver.USatResult +import org.usvm.state.GoFlowStatus +import org.usvm.state.GoState +import org.usvm.statistics.ApplicationGraph +import org.usvm.targets.UTargetsSet + +typealias GoStepScope = StepScope + +class GoInterpreter( + private val ctx: GoContext, + private val program: GoProgram, + private val applicationGraph: ApplicationGraph, + private var forkBlackList: UForkBlackList = UForkBlackList.createDefault(), +) : UInterpreter() { + fun getInitialState(method: GoMethod, targets: List = emptyList()): GoState = with(ctx) { + val initOwnership = MutabilityOwnership() + val state = GoState(ctx, initOwnership, method, targets = UTargetsSet.from(targets)) + + val solver = solver() + val model = (solver.check(state.pathConstraints) as USatResult).model + state.models = listOf(model) + + for (global in program.globals) { + var type = global.type + var ref = mkConcreteHeapRef(NULL_ADDRESS) + var depth = 0 + while (type is PointerType) { + type = type.baseType + depth++ + } + for (i in 0 until depth) { + ref = if (ref.address != NULL_ADDRESS) { + state.mkPointer(type, ref) + } else { + state.mkPointer(type) + } + type = PointerType(type) + } + addGlobal(global, ref) + } + + val entrypoint = method.blocks[0].instructions[0] + setMethodInfo(method) + state.addCall(GoCall(method, entrypoint)) + var previousEntrypoint = entrypoint + for (m in program.findInitMethods(method.packageName)+program.findOsInitMethods()) { + setMethodInfo(m) + state.addCall(GoCall(m, applicationGraph.entryPoints(m).first()), previousEntrypoint) + previousEntrypoint = m.blocks[0].instructions[0] + } + + return state + } + + override fun step(state: GoState): StepResult { + val inst = state.currentStatement + val scope = GoStepScope(state, forkBlackList) + val exprVisitor = GoExprVisitor(ctx, program, scope, applicationGraph) + val instVisitor = GoInstVisitor(ctx, program, scope, exprVisitor, applicationGraph) + + logger.debug("State {}: Step: {}", state.id, inst) + + val nextInst = next(state, inst, instVisitor) + if (nextInst !is GoNullInst) { + state.newInst(nextInst) + } + return scope.stepResult() + } + + private fun next(state: GoState, inst: GoInst, instVisitor: GoInstVisitor): GoInst { + val method = state.lastEnteredMethod + return when (state.data.flowStatus) { + GoFlowStatus.NORMAL -> inst.accept(instVisitor) + GoFlowStatus.DEFER -> { + val deferred = state.data.getDeferredCalls(method) + if (deferred.isEmpty()) { + state.data.flowStack.removeLast() + return next(state, inst, instVisitor) + } + + state.addCall(deferred.removeLast(), inst) + return GoNullInst(method) + } + + GoFlowStatus.PANIC -> { + if (!state.isExceptional) { // recovered + state.data.flowStack.removeLast() + val function = method as GoFunction + function.setRecover() + return function.recover!!.instructions.first() + } + + if (state.data.getDeferredCalls(method).isEmpty()) { + state.handlePanic() + return GoNullInst(method) + } + + state.runDefers() + return next(state, inst, instVisitor) + } + } + } + + companion object { + val logger = object : KLogging() {}.logger + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt new file mode 100644 index 0000000000..68aa1436c7 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -0,0 +1,452 @@ +package org.usvm.interpreter + +import io.ksmt.expr.KBitVec16Value +import io.ksmt.expr.KBitVec32Value +import io.ksmt.expr.KBitVec64Value +import io.ksmt.expr.KBitVec8Value +import io.ksmt.expr.KFp32Value +import io.ksmt.expr.KFp64Value +import io.ksmt.sort.KBoolSort +import io.ksmt.sort.KBv16Sort +import io.ksmt.sort.KBv32Sort +import io.ksmt.sort.KBv64Sort +import io.ksmt.sort.KBv8Sort +import io.ksmt.sort.KFp32Sort +import io.ksmt.sort.KFp64Sort +import io.ksmt.utils.asExpr +import org.jacodb.go.api.ArrayType +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoType +import org.jacodb.go.api.InterfaceType +import org.jacodb.go.api.MapType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.NullType +import org.jacodb.go.api.PointerType +import org.jacodb.go.api.SliceType +import org.jacodb.go.api.StructType +import org.jacodb.go.api.TupleType +import org.usvm.GoContext +import org.usvm.NULL_ADDRESS +import org.usvm.UAddressSort +import org.usvm.UBoolSort +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.USort +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapAnyKey +import org.usvm.api.readArrayIndex +import org.usvm.api.readArrayLength +import org.usvm.api.readField +import org.usvm.api.typeStreamOf +import org.usvm.collection.map.length.UMapLengthLValue +import org.usvm.collection.map.primitive.UMapEntryLValue +import org.usvm.collection.map.ref.URefMapEntryLValue +import org.usvm.collection.set.primitive.USetEntryLValue +import org.usvm.collection.set.primitive.setEntries +import org.usvm.collection.set.ref.URefSetEntryLValue +import org.usvm.collection.set.ref.refSetEntries +import org.usvm.interpreter.GoInterpreter.Companion.logger +import org.usvm.isTrue +import org.usvm.memory.ULValue +import org.usvm.memory.URegisterStackLValue +import org.usvm.memory.UWritableMemory +import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.mkSizeExpr +import org.usvm.model.UModelBase +import org.usvm.sampleUValue +import org.usvm.sizeSort +import org.usvm.state.GoMethodResult +import org.usvm.state.GoState +import org.usvm.type.GoBasicTypes +import org.usvm.type.GoVoidSort +import org.usvm.type.underlying +import org.usvm.types.first +import java.nio.ByteBuffer +import kotlin.random.Random +import kotlin.random.nextUInt +import kotlin.random.nextULong + +class GoTestInterpreter( + private val ctx: GoContext, +) { + fun resolve(state: GoState, method: GoMethod): ProgramExecutionResult = with(ctx) { + val model = state.models.first() + + val inputScope = MemoryScope(ctx, state, model, model) + val outputScope = MemoryScope(ctx, state, model, state.memory) + + val inputValues = List(method.parameters.size) { idx -> + val type = method.parameters[idx].type as GoType + val sort = typeToSort(type) + val expr = model.read(URegisterStackLValue(sort, idx)) + inputScope.convertExpr(expr, type) + } + val inputModel = InputModel(inputValues) + + return if (state.isExceptional) { + val panic = state.methodResult as GoMethodResult.Panic + UnsuccessfulExecutionResult(inputModel, outputScope.convertExpr(panic.value, panic.type)) + } else { + val result = state.methodResult as GoMethodResult.Success + val expr = result.let { outputScope.convertExpr(it.value, it.type) } + val outputModel = OutputModel(expr) + + SuccessfulExecutionResult(inputModel, outputModel) + } + } + + private class MemoryScope( + private val ctx: GoContext, + private val state: GoState, + private val model: UModelBase, + private val memory: UWritableMemory, + ) { + fun convertExpr(expr: UExpr, baseType: GoType): Any = when (expr.sort) { + is GoVoidSort -> "" + is KBoolSort -> resolveBool(expr) + is KBv8Sort -> resolveBv8(expr) + is KBv16Sort -> resolveBv16(expr) + is KBv32Sort -> resolveBv32(expr).let { if (baseType == GoBasicTypes.RUNE) Char(it) else it } + is KBv64Sort -> resolveBv64(expr) + is KFp32Sort -> resolveFp32(expr) + is KFp64Sort -> resolveFp64(expr) + is UAddressSort -> when (expr.sort) { + ctx.addressSort -> { + val h = expr.asExpr(ctx.addressSort) + val named = baseType is NamedType + val prefix = if (named) (baseType as NamedType).name + ": " else "" + prefix + when (val type = baseType.underlying()) { + GoBasicTypes.STRING -> if (named) resolveBoxed(h, type) else resolveString(h, type) + is BasicType -> resolveBoxed(h, type) + is ArrayType -> if (named) resolveBoxed(h, type) else resolveArray(h, type, type.len, type.elementType) + is SliceType -> if (named) resolveBoxed(h, type) else resolveSlice(h, type, type.elementType) + is MapType -> if (named) resolveBoxed(h, type) else resolveMap(h, type, type.keyType, type.valueType) + is TupleType -> resolveTuple(h, type) + is StructType -> if (named) resolveBoxed(h, type) else resolveStruct(h, type) + is InterfaceType -> resolveInterface(h) + is NullType -> null + is PointerType -> resolvePointer(h, type.baseType) + else -> Any() + } + } + + else -> Any() + } + + else -> Any() + } + + fun resolveBool(expr: UExpr) = model.eval(expr).asExpr(ctx.boolSort).isTrue + + fun resolveBv8(expr: UExpr) = (model.eval(expr) as KBitVec8Value).byteValue + + fun resolveBv16(expr: UExpr) = (model.eval(expr) as KBitVec16Value).shortValue + + fun resolveBv32(expr: UExpr) = (model.eval(expr) as KBitVec32Value).intValue + + fun resolveBv64(expr: UExpr) = (model.eval(expr) as KBitVec64Value).longValue + + fun resolveFp32(expr: UExpr) = (model.eval(expr) as KFp32Value).value + + fun resolveFp64(expr: UExpr) = (model.eval(expr) as KFp64Value).value + + fun resolveSize(expr: UExpr) = (model.eval(expr) as KBitVec32Value).numberValue + + fun resolveString(string: UHeapRef, arrayType: GoType): String = with(ctx) { + if (string == mkConcreteHeapRef(NULL_ADDRESS) || string == nullRef) { + return "" + } + + val lengthUExpr = memory.readArrayLength(string, arrayType, sizeSort) + val length = clipArrayLength(resolveSize(lengthUExpr)) + + val buffer = ByteBuffer.allocate(length * Byte.SIZE_BYTES) + for (i in 0..? = with(ctx) { + if (array == mkConcreteHeapRef(NULL_ADDRESS) || array == nullRef) { + return null + } + + val length = clipArrayLength(len.toInt()) + val sort = typeToSort(elementType) + return List(length) { idx -> + val element = memory.readArrayIndex(array, mkSizeExpr(idx), arrayType, sort) + convertExpr(element, elementType) + } + } + + fun resolveSlice(slice: UHeapRef, sliceType: GoType, elementType: GoType): List? = with(ctx) { + if (slice == mkConcreteHeapRef(NULL_ADDRESS) || slice == nullRef) { + return null + } + + val lengthUExpr = memory.readArrayLength(slice, sliceType, sizeSort) + val length = clipArrayLength(resolveSize(lengthUExpr)) + val sort = typeToSort(elementType) + return List(length) { idx -> + val element = memory.readArrayIndex(slice, mkSizeExpr(idx), sliceType, sort) + convertExpr(element, elementType) + } + } + + fun resolveMap(map: UHeapRef, mapType: GoType, keyType: GoType, valueType: GoType): Map? = with(ctx) { + if (map == mkConcreteHeapRef(NULL_ADDRESS) || map == nullRef) { + return null + } + + val keySort = typeToSort(keyType) + val valueSort = typeToSort(valueType) + + val isRefSet = keySort == addressSort + + val addToMap: (MutableMap, Set>) -> Unit = { m, s -> + m.putAll(s.associate { entry -> + val key = when (entry) { + is URefSetEntryLValue<*> -> entry.setElement + is USetEntryLValue<*, *, *> -> entry.setElement + else -> throw IllegalStateException() + } + + val lvalue = if (isRefSet) { + URefMapEntryLValue(valueSort, map, key.asExpr(addressSort), mapType) + } else { + UMapEntryLValue(keySort, valueSort, map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) + } + val value = memory.read(lvalue) + convertExpr(key, keyType) to convertExpr(value, valueType) + }) + } + val getEntries: (UHeapRef) -> Set> = { + if (isRefSet) { + memory.refSetEntries(it, mapType) + } else { + memory.setEntries(it, mapType, keySort, USizeExprKeyInfo()) + }.entries + } + + val length = clipArrayLength(resolveSize(memory.read(UMapLengthLValue(map, mapType, sizeSort)))) + + val result = mutableMapOf() + getEntries(map).also { addToMap(result, it) } + getEntries(model.eval(map)).also { addToMap(result, it) } + + if (length > result.size) { + val diff = length - result.size + val rng = RNG(keyType as BasicType) + for (i in 0 until diff) { + val key = if (isRefSet) { + convertExpr(state.symbolicObjectMapAnyKey(map, mapType), keyType) + } else { + rng.generateUniqueMapKey(result) + } + val value = convertExpr(valueSort.sampleUValue(), valueType) + result[key] = value + } + } + + return result + } + + fun resolveTuple(tuple: UHeapRef, tupleType: TupleType): List? = with(ctx) { + if (tuple == mkConcreteHeapRef(NULL_ADDRESS) || tuple == nullRef) { + return null + } + + return List(tupleType.types.size) { + val sort = typeToSort(tupleType.types[it]) + convertExpr(memory.readField(tuple, it, sort), tupleType.types[it]) + } + } + + fun resolveStruct(struct: UHeapRef, structType: StructType): Map? = with(ctx) { + if (struct == mkConcreteHeapRef(NULL_ADDRESS) || struct == nullRef) { + return null + } + + return structType.fields?.mapIndexed { idx, type -> idx to type }?.associate { + Pair("field${it.first}", convertExpr(memory.readField(struct, it.first, typeToSort(it.second)), it.second)) + } + } + + fun resolveInterface(iface: UHeapRef): Any? = with(ctx) { + if (iface == mkConcreteHeapRef(NULL_ADDRESS) || iface == nullRef) { + return null + } + + val type = memory.typeStreamOf(iface).first() + val index = 0 + return convertExpr(memory.readField(iface, index, typeToSort(type)), type) + } + + fun resolvePointer(pointer: UHeapRef, baseType: GoType): Any? = with(ctx) { + if (pointer == mkConcreteHeapRef(NULL_ADDRESS) || pointer == nullRef) { + return null + } + + val index = 0 + val expr = memory.readField(pointer, index, ctx.typeToSort(baseType)) + return "&" + convertExpr(expr, baseType) + } + + fun resolveBoxed(value: UHeapRef, type: GoType): Any? = with(ctx) { + if (value == mkConcreteHeapRef(NULL_ADDRESS)) { + return null + } + + val index = 0 + return convertExpr(memory.readField(value, index, typeToSort(type)), type) + } + } + + companion object { + fun clipArrayLength(length: Int): Int = + when { + length in 0..MAX_ARRAY_LENGTH -> length + + length > MAX_ARRAY_LENGTH -> { + logger.warn { "Array length exceeds $MAX_ARRAY_LENGTH: $length" } + MAX_ARRAY_LENGTH + } + + else -> { + logger.warn { "Negative array length: $length" } + 0 + } + } + + private const val MAX_ARRAY_LENGTH = 10_000 + } +} + +sealed interface ProgramExecutionResult + +class InputModel( + private val arguments: List +) { + override fun toString(): String { + return buildString { + appendLine("InputModel") + val arguments = arguments.joinToString(", ", "Arguments [", "]") + appendLine(arguments.prependIndent("\t")) + } + } +} + +class OutputModel( + private val returnExpr: Any? +) { + override fun toString(): String { + return buildString { + appendLine("OutputModel") + val returnString = "Return [$returnExpr]" + appendLine(returnString.prependIndent("\t")) + } + } +} + +class SuccessfulExecutionResult( + private val inputModel: InputModel, + private val outputModel: OutputModel +) : ProgramExecutionResult { + override fun toString(): String { + return buildString { + appendLine("================================================================") + appendLine("Successful Execution") + appendLine("----------------------------------------------------------------") + appendLine(inputModel.toString()) + appendLine("----------------------------------------------------------------") + appendLine(outputModel.toString()) + appendLine("================================================================") + } + } +} + +class UnsuccessfulExecutionResult( + private val inputModel: InputModel, + private val result: Any?, +) : ProgramExecutionResult { + override fun toString(): String { + return buildString { + appendLine("================================================================") + appendLine("Unsuccessful Execution") + appendLine("----------------------------------------------------------------") + appendLine(inputModel.toString()) + appendLine("----------------------------------------------------------------") + appendLine(result) + appendLine("================================================================") + } + } +} + +class RNG( + type: BasicType, + seed: Long = 0, + private val maxAttempts: Long = 10 +) { + private val random = Random(seed) + + private val generate: () -> Any = when (type) { + GoBasicTypes.INT8 -> { + { random.nextBytes(1).first() } + } + + GoBasicTypes.UINT8 -> { + { random.nextBytes(1).first().toUByte() } + } + + GoBasicTypes.INT16 -> { + { random.nextInt().toShort() } + } + + GoBasicTypes.UINT16 -> { + { random.nextInt().toUShort() } + } + + GoBasicTypes.INT, GoBasicTypes.INT32 -> { + { random.nextInt() } + } + + GoBasicTypes.UINT, GoBasicTypes.UINT32 -> { + { random.nextUInt() } + } + + GoBasicTypes.INT64 -> { + { random.nextLong() } + } + + GoBasicTypes.UINT64, GoBasicTypes.UINTPTR -> { + { random.nextULong() } + } + + GoBasicTypes.FLOAT32 -> { + { random.nextFloat() } + } + + GoBasicTypes.FLOAT64 -> { + { random.nextDouble() } + } + + else -> throw IllegalStateException() + } + + fun generateUniqueMapKey(map: Map): Any? { + for (i in 0 until maxAttempts) { + val key = generate() + if (!map.containsKey(key)) { + return key + } + } + + return null + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt new file mode 100644 index 0000000000..ffd288e1fe --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt @@ -0,0 +1,464 @@ +package org.usvm.model + +import org.jacodb.go.api.ArrayType +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.ChanType +import org.jacodb.go.api.GoAddExpr +import org.jacodb.go.api.GoAllocExpr +import org.jacodb.go.api.GoAndExpr +import org.jacodb.go.api.GoAndNotExpr +import org.jacodb.go.api.GoBasicBlock +import org.jacodb.go.api.GoBinaryExpr +import org.jacodb.go.api.GoBool +import org.jacodb.go.api.GoBuiltin +import org.jacodb.go.api.GoCallExpr +import org.jacodb.go.api.GoChangeInterfaceExpr +import org.jacodb.go.api.GoChangeTypeExpr +import org.jacodb.go.api.GoConditionExpr +import org.jacodb.go.api.GoConvertExpr +import org.jacodb.go.api.GoDeferInst +import org.jacodb.go.api.GoDivExpr +import org.jacodb.go.api.GoEqlExpr +import org.jacodb.go.api.GoExtractExpr +import org.jacodb.go.api.GoFieldAddrExpr +import org.jacodb.go.api.GoFieldExpr +import org.jacodb.go.api.GoFloat32 +import org.jacodb.go.api.GoFloat64 +import org.jacodb.go.api.GoFreeVar +import org.jacodb.go.api.GoFunction +import org.jacodb.go.api.GoGeqExpr +import org.jacodb.go.api.GoGlobal +import org.jacodb.go.api.GoGtrExpr +import org.jacodb.go.api.GoIfInst +import org.jacodb.go.api.GoIndexAddrExpr +import org.jacodb.go.api.GoIndexExpr +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoInstLocation +import org.jacodb.go.api.GoInstLocationImpl +import org.jacodb.go.api.GoInstRef +import org.jacodb.go.api.GoInt +import org.jacodb.go.api.GoInt16 +import org.jacodb.go.api.GoInt32 +import org.jacodb.go.api.GoInt64 +import org.jacodb.go.api.GoInt8 +import org.jacodb.go.api.GoJumpInst +import org.jacodb.go.api.GoLeqExpr +import org.jacodb.go.api.GoLookupExpr +import org.jacodb.go.api.GoLssExpr +import org.jacodb.go.api.GoMakeClosureExpr +import org.jacodb.go.api.GoMakeInterfaceExpr +import org.jacodb.go.api.GoMakeMapExpr +import org.jacodb.go.api.GoMakeSliceExpr +import org.jacodb.go.api.GoMapUpdateInst +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoModExpr +import org.jacodb.go.api.GoMulExpr +import org.jacodb.go.api.GoNeqExpr +import org.jacodb.go.api.GoNextExpr +import org.jacodb.go.api.GoNullConstant +import org.jacodb.go.api.GoNullInst +import org.jacodb.go.api.GoOrExpr +import org.jacodb.go.api.GoPanicInst +import org.jacodb.go.api.GoParameter +import org.jacodb.go.api.GoPhiExpr +import org.jacodb.go.api.GoRangeExpr +import org.jacodb.go.api.GoReturnInst +import org.jacodb.go.api.GoRunDefersInst +import org.jacodb.go.api.GoShlExpr +import org.jacodb.go.api.GoShrExpr +import org.jacodb.go.api.GoSliceExpr +import org.jacodb.go.api.GoSliceToArrayPointerExpr +import org.jacodb.go.api.GoStoreInst +import org.jacodb.go.api.GoStringConstant +import org.jacodb.go.api.GoSubExpr +import org.jacodb.go.api.GoType +import org.jacodb.go.api.GoTypeAssertExpr +import org.jacodb.go.api.GoUInt +import org.jacodb.go.api.GoUInt16 +import org.jacodb.go.api.GoUInt32 +import org.jacodb.go.api.GoUInt64 +import org.jacodb.go.api.GoUInt8 +import org.jacodb.go.api.GoUnArrowExpr +import org.jacodb.go.api.GoUnMulExpr +import org.jacodb.go.api.GoUnNotExpr +import org.jacodb.go.api.GoUnSubExpr +import org.jacodb.go.api.GoUnXorExpr +import org.jacodb.go.api.GoUnaryExpr +import org.jacodb.go.api.GoValue +import org.jacodb.go.api.GoVar +import org.jacodb.go.api.GoXorExpr +import org.jacodb.go.api.InterfaceType +import org.jacodb.go.api.MapType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.NullType +import org.jacodb.go.api.OpaqueType +import org.jacodb.go.api.PointerType +import org.jacodb.go.api.SignatureType +import org.jacodb.go.api.SliceType +import org.jacodb.go.api.StructType +import org.jacodb.go.api.TupleType +import org.jacodb.go.api.TypeParam +import org.jacodb.go.api.UnionType +import org.usvm.GoPackage +import org.usvm.type.GoBasicTypes +import org.usvm.type.GoTypes + +object Converter { + private val typesMap: MutableMap = mutableMapOf() + + fun unpack(pkg: Package): GoPackage { + typesMap.putAll(pkg.types.mapValues { type -> unpack(pkg.types, type.value) }) + val methods = pkg.members.filterIsInstance().map { function -> + GoFunction( + SignatureType(TupleType(function.parameters.map { getType(it.goType) }), TupleType(function.returnTypes.map(::getType))), + function.parameters.mapIndexed(this::unpack), + function.name, + emptyList(), + pkg.name, + function.freeVars.map { unpack(it) as GoFreeVar }, + function.returnTypes.map(::getType), + ).also { + it.blocks = function.basicBlocks.map { block -> unpack(it, block) } + if (function.recover != null) { + it.recover = unpack(it, function.recover) + } + } + } + val globals = pkg.members.filterIsInstance().map { global -> + GoGlobal(global.index, global.name, getType(global.goType)) + } + return GoPackage(pkg.name, methods, globals, typesMap) + } + + private fun unpack(index: Int, param: Value): GoParameter { + return GoParameter(index, param.name, getType(param.goType)) + } + + private fun unpack(function: GoFunction, block: Member.BasicBlock): GoBasicBlock { + return GoBasicBlock(block.index, block.next, block.prev, block.instructions.map { unpack(function, it) }) + } + + private fun unpack(function: GoFunction, inst: Instruction): GoInst { + val loc = GoInstLocationImpl(function, inst.block, inst.line) + return when (inst) { + is Instruction.Alloc -> GoAllocExpr(loc, getType(inst.goType), inst.register, inst.comment).toAssignInst() + is Instruction.BinOp -> unpack(loc, inst) + is Instruction.Call -> unpack(loc, inst) + is Instruction.ChangeInterface -> GoChangeInterfaceExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + is Instruction.ChangeType -> GoChangeTypeExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + is Instruction.Convert -> GoConvertExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + is Instruction.DebugRef -> unsupportedInstruction(function) + is Instruction.Defer -> GoDeferInst(loc, unpack(inst.value), inst.args.map(::unpack)) + is Instruction.Extract -> unpack(loc, inst) + is Instruction.Field -> GoFieldExpr(loc, getType(inst.goType), unpack(inst.struct), inst.field, inst.register).toAssignInst() + is Instruction.FieldAddr -> GoFieldAddrExpr(loc, getType(inst.goType), unpack(inst.struct), inst.field, inst.register).toAssignInst() + is Instruction.Go -> unsupportedInstruction(function) + is Instruction.If -> GoIfInst(loc, unpack(inst.condition) as GoConditionExpr, GoInstRef(inst.trueBranch), GoInstRef(inst.falseBranch)) + is Instruction.Index -> unpack(loc, inst) + is Instruction.IndexAddr -> unpack(loc, inst) + is Instruction.Jump -> GoJumpInst(loc, GoInstRef(inst.index)) + is Instruction.Lookup -> unpack(loc, inst) + is Instruction.MakeChan -> unsupportedInstruction(function) + is Instruction.MakeClosure -> unpack(loc, inst) + is Instruction.MakeInterface -> GoMakeInterfaceExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + is Instruction.MakeMap -> GoMakeMapExpr(loc, getType(inst.goType), unpack(inst.reserve), inst.register).toAssignInst() + is Instruction.MakeSlice -> unpack(loc, inst) + is Instruction.MapUpdate -> GoMapUpdateInst(loc, unpack(inst.map), unpack(inst.key), unpack(inst.value)) + is Instruction.MultiConvert -> unsupportedInstruction(function) + is Instruction.Next -> GoNextExpr(loc, getType(inst.goType), unpack(inst.iter), inst.register).toAssignInst() + is Instruction.Panic -> GoPanicInst(loc, unpack(inst.value)) + is Instruction.Phi -> GoPhiExpr(loc, getType(inst.goType), inst.edges.map(this::unpack), inst.register).toAssignInst() + is Instruction.Range -> GoRangeExpr(loc, getType(inst.goType), unpack(inst.collection), inst.register).toAssignInst() + is Instruction.Return -> GoReturnInst(loc, inst.results.map(this::unpack)) + is Instruction.RunDefers -> GoRunDefersInst(loc) + is Instruction.Select -> unsupportedInstruction(function) + is Instruction.Send -> unsupportedInstruction(function) + is Instruction.Slice -> unpack(loc, inst) + is Instruction.SliceToArrayPointer -> unpack(loc, inst) + is Instruction.Store -> GoStoreInst(loc, unpack(inst.addr), unpack(inst.value)) + is Instruction.TypeAssert -> unpack(loc, inst) + is Instruction.UnOp -> unpack(loc, inst) + } + } + + private fun unpack(location: GoInstLocation, binOp: Instruction.BinOp): GoInst { + val binaryExprConstructors = mapOf>( + "&&" to ::GoAndExpr, + "&^" to ::GoAndNotExpr, + "||" to ::GoOrExpr, + "^" to ::GoXorExpr, + "==" to ::GoEqlExpr, + "!=" to ::GoNeqExpr, + "<" to ::GoLssExpr, + "<=" to ::GoLeqExpr, + ">" to ::GoGtrExpr, + ">=" to ::GoGeqExpr, + "+" to ::GoAddExpr, + "-" to ::GoSubExpr, + "*" to ::GoMulExpr, + "/" to ::GoDivExpr, + "%" to ::GoModExpr, + "<<" to ::GoShlExpr, + ">>" to ::GoShrExpr, + ) + + if (binaryExprConstructors.containsKey(binOp.op)) { + return binaryExprConstructors[binOp.op]!!( + location, + getType(binOp.goType), + unpack(binOp.first), + unpack(binOp.second), + binOp.register + ).toAssignInst() + } + + return GoNullInst(location.method) + } + + private fun unpack(location: GoInstLocation, call: Instruction.Call): GoInst { + return GoCallExpr( + location, + getType(call.goType), + unpack(call.value), + call.args.map(this::unpack), + if (call.method == "") null else functionAlias(call.method), + call.register + ).toAssignInst() + } + + private fun unpack(location: GoInstLocation, extract: Instruction.Extract): GoInst { + return GoExtractExpr(location, getType(extract.goType), unpack(extract.tuple), extract.index, extract.register).toAssignInst() + } + + private fun unpack(location: GoInstLocation, index: Instruction.Index): GoInst { + return GoIndexExpr( + location, + getType(index.goType), + unpack(index.collection), + unpack(index.index), + index.register + ).toAssignInst() + } + + private fun unpack(loc: GoInstLocation, indexAddr: Instruction.IndexAddr): GoInst { + return GoIndexAddrExpr( + loc, + getType(indexAddr.goType), + unpack(indexAddr.collection), + unpack(indexAddr.index), + indexAddr.register + ).toAssignInst() + } + + private fun unpack(location: GoInstLocation, lookup: Instruction.Lookup): GoInst { + return GoLookupExpr( + location, + getType(lookup.goType), + unpack(lookup.map), + unpack(lookup.key), + lookup.register, + lookup.commaOk + ).toAssignInst() + } + + private fun unpack(location: GoInstLocation, makeClosure: Instruction.MakeClosure): GoInst { + return GoMakeClosureExpr( + location, + OpaqueType(makeClosure.name), + functionAlias(makeClosure.function.name), + makeClosure.bindings.map(this::unpack), + makeClosure.register + ).toAssignInst() + } + + private fun unpack(location: GoInstLocation, makeSlice: Instruction.MakeSlice): GoInst { + return GoMakeSliceExpr( + location, + getType(makeSlice.goType), + unpack(makeSlice.len), + unpack(makeSlice.cap), + makeSlice.register + ).toAssignInst() + } + + private fun unpack(location: GoInstLocation, slice: Instruction.Slice): GoInst { + return GoSliceExpr( + location, + getType(slice.goType), + unpack(slice.collection), + unpack(slice.low), + unpack(slice.high), + unpack(slice.max), + slice.register + ).toAssignInst() + } + + private fun unpack(location: GoInstLocation, sliceToArrayPointerExpr: Instruction.SliceToArrayPointer): GoInst { + return GoSliceToArrayPointerExpr( + location, + getType(sliceToArrayPointerExpr.goType), + unpack(sliceToArrayPointerExpr.value), + sliceToArrayPointerExpr.register + ).toAssignInst() + } + + private fun unpack(value: Value): GoValue { + return when (value) { + is Value.Const -> unpack(value) + is Value.FreeVar -> GoFreeVar(value.index, value.name, getType(value.goType)) + is Value.Global -> GoGlobal(value.index, value.name, getType(value.goType)) + is Value.Parameter -> GoParameter(value.index, value.name, getType(value.goType)) + is Value.Var -> GoVar(value.name, getType(value.goType)) + is Value.MakeClosure -> GoVar(value.name, getType(value.goType)) + is Value.Function -> functionAlias(value.name) + is Value.Builtin -> GoBuiltin(value.name, getType(value.goType)) + } + } + + private fun unpack(location: GoInstLocation, typeAssert: Instruction.TypeAssert): GoInst { + return GoTypeAssertExpr( + location, + getType(typeAssert.goType), + unpack(typeAssert.value), + getType(typeAssert.assertedType), + typeAssert.register + ).toAssignInst() + } + + private fun unpack(location: GoInstLocation, unOp: Instruction.UnOp): GoInst { + val unaryExprConstructors = mapOf>( + "*" to ::GoUnMulExpr, + "!" to ::GoUnNotExpr, + "-" to ::GoUnSubExpr, + "^" to ::GoUnXorExpr, + ) + + val type = getType(unOp.goType) + val arg = unpack(unOp.argument) + val name = unOp.register + + return when { + unaryExprConstructors.containsKey(unOp.op) -> unaryExprConstructors[unOp.op]!!(location, type, arg, name).toAssignInst() + unOp.op == "<-" -> GoUnArrowExpr(location, type, arg, unOp.commaOk, name).toAssignInst() + else -> GoNullInst(location.method) + } + } + + private fun unpack(value: Value.Const): GoValue { + val type = getType(value.value.type) + if (type is NamedType) { + return unpack(value, type.underlyingType, type) + } + return unpack(value, type, type) + } + + private fun unpack(value: Value.Const, basicType: GoType, type: GoType): GoValue { + val const = value.value + val string = const.value + + return when (basicType) { + GoBasicTypes.BOOL -> GoBool(string.toBooleanStrict(), type) + GoBasicTypes.INT -> GoInt(string.toLong(), type) + GoBasicTypes.INT8 -> GoInt8(string.toByte(), type) + GoBasicTypes.INT16 -> GoInt16(string.toShort(), type) + GoBasicTypes.INT32 -> GoInt32(string.toInt(), type) + GoBasicTypes.INT64 -> GoInt64(string.toLong(), type) + GoBasicTypes.UINT -> GoUInt(string.toULong(), type) + GoBasicTypes.UINT8 -> GoUInt8(string.toUByte(), type) + GoBasicTypes.UINT16 -> GoUInt16(string.toUShort(), type) + GoBasicTypes.UINT32 -> GoUInt32(string.toUInt(), type) + GoBasicTypes.UINT64, GoBasicTypes.UINTPTR -> GoUInt64(string.toULong(), type) + GoBasicTypes.FLOAT32 -> GoFloat32(string.toFloat(), type) + GoBasicTypes.FLOAT64 -> GoFloat64(string.toDouble(), type) + GoBasicTypes.RUNE -> GoInt32(string.toInt(), type) + GoBasicTypes.STRING -> GoStringConstant(unquote(string), type) + else -> GoNullConstant(basicType) + } + } + + private fun unpack(typesMap: Map, types: Collection): List { + return types.map { unpack(typesMap, it) } + } + + private fun unpack(types: Map, type: Type): GoType = internType(when (type) { + is Type.Alias -> unpack(types, type.from) + is Type.Array -> ArrayType(type.len, unpack(types, type.elem)) + is Type.Basic -> unpack(type.name) + is Type.Chan -> ChanType(type.dir.toLong(), unpack(types, type.elem)) + is Type.Interface -> InterfaceType(type.methods, type.name) + is Type.Map -> MapType(unpack(types, type.key), unpack(types, type.elem)) + is Type.Named -> { + val named = NamedType(NullType(), type.name, type.methods) + typesMap[type.name] = named + named.underlyingType = unpack(types, type.underlying) + named + } + + is Type.Opaque -> OpaqueType(type.name) + is Type.Pointer -> PointerType(unpack(types, type.elem)) + is Type.Signature -> SignatureType(TupleType(unpack(types, type.params)), TupleType(unpack(types, type.results))) + is Type.Slice -> SliceType(unpack(types, type.elem)) + is Type.Struct -> StructType(unpack(types, type.fields), null) + is Type.Tuple -> TupleType(unpack(types, type.elems)) + is Type.TypeParam -> TypeParam(type.name) + is Type.Union -> UnionType(emptyList()) // TODO(buraindo) proper type list when generics are supported + }) + + private fun unpack(types: Map, type: String): GoType { + if (type in typesMap) { + return typesMap[type]!! + } + return unpack(types, types[type]!!) + } + + private fun unpack(name: String) = when (name) { + GoTypes.BOOL -> GoBasicTypes.BOOL + GoTypes.INT -> GoBasicTypes.INT + GoTypes.INT8 -> GoBasicTypes.INT8 + GoTypes.INT16 -> GoBasicTypes.INT16 + GoTypes.INT32 -> GoBasicTypes.INT32 + GoTypes.INT64 -> GoBasicTypes.INT64 + GoTypes.UINT -> GoBasicTypes.UINT + GoTypes.UINT8 -> GoBasicTypes.UINT8 + GoTypes.UINT16 -> GoBasicTypes.UINT16 + GoTypes.UINT32 -> GoBasicTypes.UINT32 + GoTypes.UINT64 -> GoBasicTypes.UINT64 + GoTypes.FLOAT32 -> GoBasicTypes.FLOAT32 + GoTypes.FLOAT64 -> GoBasicTypes.FLOAT64 + GoTypes.STRING -> GoBasicTypes.STRING + GoTypes.BYTE -> GoBasicTypes.UINT8 + GoTypes.RUNE -> GoBasicTypes.RUNE + GoTypes.UINTPTR -> GoBasicTypes.UINTPTR + GoTypes.UNTYPED_BOOL -> GoBasicTypes.BOOL + GoTypes.UNTYPED_INT -> GoBasicTypes.INT + GoTypes.UNTYPED_RUNE -> GoBasicTypes.RUNE + GoTypes.UNTYPED_FLOAT -> GoBasicTypes.FLOAT64 + GoTypes.UNTYPED_STRING -> GoBasicTypes.STRING + GoTypes.UNSAFE_POINTER -> GoBasicTypes.UNSAFE_POINTER + else -> BasicType("unknown") + } + + private fun getType(type: String): GoType = typesMap[type]!! + + private fun functionAlias(name: String): GoFunction { + return GoFunction(OpaqueType(name), emptyList(), name, emptyList(), "", emptyList(), emptyList()) + } + + private fun unsupportedInstruction(parent: GoMethod): GoInst { + return GoNullInst(parent) + } + + private val internTypeMap = mutableMapOf() + + private fun internType(type: GoType): GoType { + return internTypeMap.computeIfAbsent(type) { type } + } + + private fun unquote(s: String): String { + if (s.startsWith("\"") && s.endsWith("\"")) { + return s.substring(1, s.length - 1) + } + + return s + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/model/Model.kt b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt new file mode 100644 index 0000000000..ab136fa5cc --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt @@ -0,0 +1,642 @@ +package org.usvm.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Package( + val name: String, + val members: List, + val types: Map +) + +@Serializable +sealed interface Member { + val name: String + + @Serializable + @SerialName("NamedConst") + data class NamedConst( + override val name: String, + val value: NamedConstValue, + ) : Member + + @Serializable + @SerialName("Function") + data class Function( + override val name: String, + @SerialName("basic_blocks") val basicBlocks: List, + val parameters: List, + @SerialName("free_vars") val freeVars: List, + @SerialName("return_types") val returnTypes: List, + val recover: BasicBlock? = null, + ) : Member + + @Serializable + @SerialName("Global") + data class Global( + override val name: String, + val index: Int, + @SerialName("go_type") val goType: String, + ) : Member + + @Serializable + @SerialName("Type") + data class Type(override val name: String) : Member + + @Serializable + data class BasicBlock( + val index: Int, + val instructions: List, + val prev: List, + val next: List, + ) +} + +@Serializable +data class NamedConstValue( + val type: String, + val value: String, +) + +@Serializable +sealed interface Instruction { + val name: String + val block: Int + val line: Int + + @Serializable + @SerialName("DebugRef") + data class DebugRef( + override val name: String, + override val block: Int, + override val line: Int, + ) : Instruction + + @Serializable + @SerialName("UnOp") + data class UnOp( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val op: String, + val register: String, + val argument: Value, + @SerialName("comma_ok") val commaOk: Boolean, + ) : Instruction + + @Serializable + @SerialName("BinOp") + data class BinOp( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val op: String, + val register: String, + val first: Value, + val second: Value, + ) : Instruction + + @Serializable + @SerialName("Call") + data class Call( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val value: Value, + val method: String, + val args: List, + ) : Instruction + + @Serializable + @SerialName("ChangeInterface") + data class ChangeInterface( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("ChangeType") + data class ChangeType( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("Convert") + data class Convert( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("SliceToArrayPointer") + data class SliceToArrayPointer( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("MakeInterface") + data class MakeInterface( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("Extract") + data class Extract( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val tuple: Value, + val index: Int, + ) : Instruction + + @Serializable + @SerialName("Slice") + data class Slice( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val collection: Value, + val low: Value, + val high: Value, + val max: Value, + ) : Instruction + + @Serializable + @SerialName("Return") + data class Return( + override val name: String, + override val block: Int, + override val line: Int, + val results: List, + ) : Instruction + + @Serializable + @SerialName("RunDefers") + data class RunDefers( + override val name: String, + override val block: Int, + override val line: Int, + ) : Instruction + + @Serializable + @SerialName("Panic") + data class Panic( + override val name: String, + override val block: Int, + override val line: Int, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("Send") + data class Send( + override val name: String, + override val block: Int, + override val line: Int, + ) : Instruction + + @Serializable + @SerialName("Store") + data class Store( + override val name: String, + override val block: Int, + override val line: Int, + val addr: Value, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("If") + data class If( + override val name: String, + override val block: Int, + override val line: Int, + val condition: Value, + @SerialName("true_branch") val trueBranch: Int, + @SerialName("false_branch") val falseBranch: Int, + ) : Instruction + + @Serializable + @SerialName("Jump") + data class Jump( + override val name: String, + override val block: Int, + override val line: Int, + val index: Int, + ) : Instruction + + @Serializable + @SerialName("Defer") + data class Defer( + override val name: String, + override val block: Int, + override val line: Int, + val value: Value, + val method: String, + val args: List, + ) : Instruction + + @Serializable + @SerialName("Go") + data class Go( + override val name: String, + override val block: Int, + override val line: Int, + ) : Instruction + + @Serializable + @SerialName("MakeChan") + data class MakeChan( + override val name: String, + override val block: Int, + override val line: Int, + ) : Instruction + + @Serializable + @SerialName("Alloc") + data class Alloc( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val comment: String, + ) : Instruction + + @Serializable + @SerialName("MakeSlice") + data class MakeSlice( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val len: Value, + val cap: Value, + ) : Instruction + + @Serializable + @SerialName("MakeMap") + data class MakeMap( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val reserve: Value, + ) : Instruction + + @Serializable + @SerialName("Range") + data class Range( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val collection: Value, + ) : Instruction + + @Serializable + @SerialName("Next") + data class Next( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val iter: Value, + @SerialName("is_string") val isString: Boolean, + ) : Instruction + + @Serializable + @SerialName("FieldAddr") + data class FieldAddr( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val struct: Value, + val field: Int, + ) : Instruction + + @Serializable + @SerialName("Field") + data class Field( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val struct: Value, + val field: Int, + ) : Instruction + + @Serializable + @SerialName("IndexAddr") + data class IndexAddr( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val collection: Value, + val index: Value, + ) : Instruction + + @Serializable + @SerialName("Index") + data class Index( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val collection: Value, + val index: Value, + ) : Instruction + + @Serializable + @SerialName("Lookup") + data class Lookup( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val map: Value, + val key: Value, + @SerialName("comma_ok") val commaOk: Boolean, + ) : Instruction + + @Serializable + @SerialName("MapUpdate") + data class MapUpdate( + override val name: String, + override val block: Int, + override val line: Int, + val map: Value, + val key: Value, + val value: Value, + ) : Instruction + + @Serializable + @SerialName("TypeAssert") + data class TypeAssert( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val value: Value, + @SerialName("asserted_type") val assertedType: String, + ) : Instruction + + @Serializable + @SerialName("MakeClosure") + data class MakeClosure( + override val name: String, + override val block: Int, + override val line: Int, + val register: String, + val function: Value, + val bindings: List, + ) : Instruction + + @Serializable + @SerialName("Phi") + data class Phi( + override val name: String, + override val block: Int, + override val line: Int, + @SerialName("go_type") val goType: String, + val register: String, + val edges: List, + ) : Instruction + + @Serializable + @SerialName("Select") + data class Select( + override val name: String, + override val block: Int, + override val line: Int, + ) : Instruction + + @Serializable + @SerialName("MultiConvert") + data class MultiConvert( + override val name: String, + override val block: Int, + override val line: Int, + ) : Instruction +} + +@Serializable +sealed interface Value { + val goType: String + val name: String + + @Serializable + @SerialName("Const") + data class Const( + @SerialName("go_type") override val goType: String, + override val name: String, + val value: NamedConstValue, + ) : Value + + @Serializable + @SerialName("Global") + data class Global( + @SerialName("go_type") override val goType: String, + override val name: String, + val index: Int, + ) : Value + + @Serializable + @SerialName("Parameter") + data class Parameter( + @SerialName("go_type") override val goType: String, + override val name: String, + val index: Int, + ) : Value + + @Serializable + @SerialName("FreeVar") + data class FreeVar( + @SerialName("go_type") override val goType: String, + override val name: String, + val index: Int, + ) : Value + + @Serializable + @SerialName("Var") + data class Var( + @SerialName("go_type") override val goType: String, + override val name: String, + ) : Value + + @Serializable + @SerialName("Function") + data class Function( + @SerialName("go_type") override val goType: String, + override val name: String, + ) : Value + + @Serializable + @SerialName("MakeClosure") + data class MakeClosure( + @SerialName("go_type") override val goType: String, + override val name: String, + ) : Value + + @Serializable + @SerialName("Builtin") + data class Builtin( + @SerialName("go_type") override val goType: String, + override val name: String, + ) : Value +} + +@Serializable +sealed interface Type { + val name: String + + @Serializable + @SerialName("Alias") + data class Alias( + override val name: String, + val from: String, + ) : Type + + @Serializable + @SerialName("Array") + data class Array( + override val name: String, + val len: Long, + val elem: String, + ) : Type + + @Serializable + @SerialName("Basic") + data class Basic( + override val name: String, + ) : Type + + @Serializable + @SerialName("Chan") + data class Chan( + override val name: String, + val dir: Int, + val elem: String, + ) : Type + + @Serializable + @SerialName("Interface") + data class Interface( + override val name: String, + val methods: List, + ) : Type + + @Serializable + @SerialName("Map") + data class Map( + override val name: String, + val key: String, + val elem: String, + ) : Type + + @Serializable + @SerialName("Named") + data class Named( + override val name: String, + val underlying: String, + val methods: List, + ) : Type + + @Serializable + @SerialName("Opaque") + data class Opaque( + override val name: String, + ) : Type + + @Serializable + @SerialName("Pointer") + data class Pointer( + override val name: String, + val elem: String, + ) : Type + + @Serializable + @SerialName("Signature") + data class Signature( + override val name: String, + val params: List, + val results: List, + ) : Type + + @Serializable + @SerialName("Slice") + data class Slice( + override val name: String, + val elem: String, + ) : Type + + @Serializable + @SerialName("Struct") + data class Struct( + override val name: String, + val fields: List + ) : Type + + @Serializable + @SerialName("Tuple") + data class Tuple( + override val name: String, + val elems: List + ) : Type + + @Serializable + @SerialName("TypeParam") + data class TypeParam( + override val name: String, + ) : Type + + @Serializable + @SerialName("Union") + data class Union( + override val name: String, + ) : Type +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/model/Parser.kt b/usvm-go/src/main/kotlin/org/usvm/model/Parser.kt new file mode 100644 index 0000000000..a918027316 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/model/Parser.kt @@ -0,0 +1,98 @@ +package org.usvm.model + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import java.io.File + +class Parser { + private val jsonSerializer = Json { + serializersModule = SerializersModule { + polymorphic(Member::class) { + subclass(Member.NamedConst::class, Member.NamedConst.serializer()) + subclass(Member.Function::class, Member.Function.serializer()) + subclass(Member.Global::class, Member.Global.serializer()) + subclass(Member.Type::class, Member.Type.serializer()) + } + + polymorphic(Instruction::class) { + subclass(Instruction.DebugRef::class, Instruction.DebugRef.serializer()) + subclass(Instruction.UnOp::class, Instruction.UnOp.serializer()) + subclass(Instruction.BinOp::class, Instruction.BinOp.serializer()) + subclass(Instruction.Call::class, Instruction.Call.serializer()) + subclass(Instruction.ChangeInterface::class, Instruction.ChangeInterface.serializer()) + subclass(Instruction.ChangeType::class, Instruction.ChangeType.serializer()) + subclass(Instruction.Convert::class, Instruction.Convert.serializer()) + subclass(Instruction.SliceToArrayPointer::class, Instruction.SliceToArrayPointer.serializer()) + subclass(Instruction.MakeInterface::class, Instruction.MakeInterface.serializer()) + subclass(Instruction.Extract::class, Instruction.Extract.serializer()) + subclass(Instruction.Slice::class, Instruction.Slice.serializer()) + subclass(Instruction.Return::class, Instruction.Return.serializer()) + subclass(Instruction.RunDefers::class, Instruction.RunDefers.serializer()) + subclass(Instruction.Panic::class, Instruction.Panic.serializer()) + subclass(Instruction.Send::class, Instruction.Send.serializer()) + subclass(Instruction.Store::class, Instruction.Store.serializer()) + subclass(Instruction.If::class, Instruction.If.serializer()) + subclass(Instruction.Jump::class, Instruction.Jump.serializer()) + subclass(Instruction.Defer::class, Instruction.Defer.serializer()) + subclass(Instruction.Go::class, Instruction.Go.serializer()) + subclass(Instruction.MakeChan::class, Instruction.MakeChan.serializer()) + subclass(Instruction.Alloc::class, Instruction.Alloc.serializer()) + subclass(Instruction.MakeSlice::class, Instruction.MakeSlice.serializer()) + subclass(Instruction.MakeMap::class, Instruction.MakeMap.serializer()) + subclass(Instruction.Range::class, Instruction.Range.serializer()) + subclass(Instruction.Next::class, Instruction.Next.serializer()) + subclass(Instruction.FieldAddr::class, Instruction.FieldAddr.serializer()) + subclass(Instruction.Field::class, Instruction.Field.serializer()) + subclass(Instruction.IndexAddr::class, Instruction.IndexAddr.serializer()) + subclass(Instruction.Index::class, Instruction.Index.serializer()) + subclass(Instruction.Lookup::class, Instruction.Lookup.serializer()) + subclass(Instruction.MapUpdate::class, Instruction.MapUpdate.serializer()) + subclass(Instruction.TypeAssert::class, Instruction.TypeAssert.serializer()) + subclass(Instruction.MakeClosure::class, Instruction.MakeClosure.serializer()) + subclass(Instruction.Phi::class, Instruction.Phi.serializer()) + subclass(Instruction.Select::class, Instruction.Select.serializer()) + subclass(Instruction.MultiConvert::class, Instruction.MultiConvert.serializer()) + } + + polymorphic(Value::class) { + subclass(Value.Const::class, Value.Const.serializer()) + subclass(Value.Global::class, Value.Global.serializer()) + subclass(Value.Parameter::class, Value.Parameter.serializer()) + subclass(Value.FreeVar::class, Value.FreeVar.serializer()) + subclass(Value.Var::class, Value.Var.serializer()) + subclass(Value.Function::class, Value.Function.serializer()) + subclass(Value.MakeClosure::class, Value.MakeClosure.serializer()) + subclass(Value.Builtin::class, Value.Builtin.serializer()) + } + + polymorphic(Type::class) { + subclass(Type.Alias::class, Type.Alias.serializer()) + subclass(Type.Array::class, Type.Array.serializer()) + subclass(Type.Basic::class, Type.Basic.serializer()) + subclass(Type.Chan::class, Type.Chan.serializer()) + subclass(Type.Interface::class, Type.Interface.serializer()) + subclass(Type.Map::class, Type.Map.serializer()) + subclass(Type.Named::class, Type.Named.serializer()) + subclass(Type.Pointer::class, Type.Pointer.serializer()) + subclass(Type.Signature::class, Type.Signature.serializer()) + subclass(Type.Slice::class, Type.Slice.serializer()) + subclass(Type.Struct::class, Type.Struct.serializer()) + subclass(Type.Tuple::class, Type.Tuple.serializer()) + subclass(Type.TypeParam::class, Type.TypeParam.serializer()) + subclass(Type.Union::class, Type.Union.serializer()) + } + } + } + + fun deserialize(filename: String): Package { + val file = File(filename) + val jsonString = file.readText() + return Json.decodeFromString(jsonString) + } + + fun serialize(pkg: Package): String { + return jsonSerializer.encodeToString(pkg) + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt new file mode 100644 index 0000000000..2d4f72226d --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt @@ -0,0 +1,144 @@ +package org.usvm.operator + +import io.ksmt.utils.asExpr +import io.ksmt.utils.cast +import org.usvm.UAddressSort +import org.usvm.UBoolSort +import org.usvm.UBvSort +import org.usvm.UExpr +import org.usvm.UFpSort +import org.usvm.USort +import org.usvm.GoContext +import org.usvm.type.goCtx +import org.usvm.uctx + +sealed class GoBinaryOperator( + val onBool: GoContext.(UExpr, UExpr) -> UExpr = shouldNotBeCalled, + val onBv: GoContext.(UExpr, UExpr) -> UExpr = shouldNotBeCalled, + val onFp: GoContext.(UExpr, UExpr) -> UExpr = shouldNotBeCalled, + val onAddress: GoContext.(UExpr, UExpr) -> UExpr = shouldNotBeCalled, +) { + data object Add : GoBinaryOperator( + onBv = GoContext::mkBvAddExpr, + onFp = { lhs, rhs -> mkFpAddExpr(fpRoundingModeSortDefaultValue(), lhs, rhs) } + ) + + data object Sub : GoBinaryOperator( + onBv = GoContext::mkBvSubExpr, + onFp = { lhs, rhs -> mkFpSubExpr(fpRoundingModeSortDefaultValue(), lhs, rhs) } + ) + + data object Mul : GoBinaryOperator( + onBv = GoContext::mkBvMulExpr, + onFp = { lhs, rhs -> mkFpMulExpr(fpRoundingModeSortDefaultValue(), lhs, rhs) } + ) + + class Div(signed: Boolean) : GoBinaryOperator( + onBv = if (signed) GoContext::mkBvSignedDivExpr else GoContext::mkBvUnsignedDivExpr, + onFp = { lhs, rhs -> mkFpDivExpr(fpRoundingModeSortDefaultValue(), lhs, rhs) } + ) + + class Mod(signed: Boolean) : GoBinaryOperator( + onBv = if (signed) GoContext::mkBvSignedRemExpr else GoContext::mkBvUnsignedRemExpr, + onFp = GoContext::mkFpRemExpr, + ) + + data object And : GoBinaryOperator( + onBool = GoContext::mkAnd, + onBv = GoContext::mkBvAndExpr, + ) + + data object Or : GoBinaryOperator( + onBool = GoContext::mkOr, + onBv = GoContext::mkBvOrExpr, + ) + + data object Xor : GoBinaryOperator( + onBool = GoContext::mkXor, + onBv = GoContext::mkBvXorExpr, + ) + + data object Shl : GoBinaryOperator( + onBv = { arg, shift -> mkBvShiftLeftExpr(arg, normalizeBvShift(shift)) } + ) + + class Shr(signed: Boolean) : GoBinaryOperator( + onBv = { arg, shift -> + if (signed) mkBvArithShiftRightExpr(arg, normalizeBvShift(shift)) + else mkBvLogicalShiftRightExpr(arg, normalizeBvShift(shift)) + } + ) + + data object AndNot : GoBinaryOperator( + onBv = { lhs, rhs -> mkBvAndExpr(lhs, mkBvNegationExpr(rhs)) } + ) + + data object Eql : GoBinaryOperator( + onBool = GoContext::mkEq, + onBv = GoContext::mkEq, + onFp = GoContext::mkFpEqualExpr, + onAddress = GoContext::mkHeapRefEq, + ) + + class Lss(signed: Boolean) : GoBinaryOperator( + onBv = if (signed) GoContext::mkBvSignedLessExpr else GoContext::mkBvUnsignedLessExpr, + onFp = GoContext::mkFpLessExpr, + ) + + class Gtr(signed: Boolean) : GoBinaryOperator( + onBv = if (signed) GoContext::mkBvSignedGreaterExpr else GoContext::mkBvUnsignedGreaterExpr, + onFp = GoContext::mkFpGreaterExpr, + ) + + data object Neq : GoBinaryOperator( + onBool = { lhs, rhs -> lhs.neq(rhs) }, + onBv = { lhs, rhs -> lhs.neq(rhs) }, + onFp = { lhs, rhs -> mkFpEqualExpr(lhs, rhs).not() }, + onAddress = { lhs, rhs -> mkHeapRefEq(lhs, rhs).not() }, + ) + + class Leq(signed: Boolean) : GoBinaryOperator( + onBv = if (signed) GoContext::mkBvSignedLessOrEqualExpr else GoContext::mkBvUnsignedLessOrEqualExpr, + onFp = GoContext::mkFpLessOrEqualExpr, + ) + + class Geq(signed: Boolean) : GoBinaryOperator( + onBv = if (signed) GoContext::mkBvSignedGreaterOrEqualExpr else GoContext::mkBvUnsignedGreaterOrEqualExpr, + onFp = GoContext::mkFpGreaterOrEqualExpr + ) + + internal open operator fun invoke(lhs: UExpr, rhs: UExpr): UExpr { + val lhsSort = lhs.sort + val rhsSort = rhs.sort + + return when { + lhsSort != rhsSort -> error("Expressions sorts mismatch: $lhsSort, $rhsSort") + lhsSort is UBoolSort -> lhs.goCtx.onBool(lhs.cast(), rhs.cast()) + lhsSort is UBvSort -> lhs.goCtx.onBv(lhs.cast(), rhs.cast()) + lhsSort is UFpSort -> lhs.goCtx.onFp(lhs.cast(), rhs.cast()) + lhsSort is UAddressSort -> lhs.goCtx.onAddress(lhs.cast(), rhs.cast()) + else -> error("Unexpected sorts: $lhsSort, $rhsSort") + } + } + + companion object { + private val shouldNotBeCalled: GoContext.(UExpr, UExpr) -> UExpr = + { _, _ -> error("Should not be called") } + + internal fun normalizeBvShift(shift: UExpr): UExpr = with(shift.uctx) { + return when (shift.sort) { + bv32Sort -> { + val mask = mkBv(31) // 0b11111 + mkBvAndExpr(shift.asExpr(bv32Sort), mask).asExpr(shift.sort) + } + + bv64Sort -> { + val mask = mkBv(63L) // 0b111111 + mkBvAndExpr(shift.asExpr(bv64Sort), mask).asExpr(shift.sort) + } + + else -> error("Incorrect bv shift: $shift") + } + } + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt new file mode 100644 index 0000000000..83afc6f727 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt @@ -0,0 +1,76 @@ +package org.usvm.operator + +import io.ksmt.sort.KBoolSort +import io.ksmt.sort.KBvSort +import io.ksmt.utils.BvUtils.bvMaxValueSigned +import io.ksmt.utils.BvUtils.bvMinValueSigned +import org.usvm.UBvSort +import org.usvm.UExpr +import org.usvm.UFpSort +import org.usvm.USort + +private val intSizeBits = Int.SIZE_BITS.toUInt() + +@Suppress("UNCHECKED_CAST") +internal fun UExpr.wideTo32BitsIfNeeded(signed: Boolean): UExpr = + with(ctx) { + when (val sort = sort) { + boolSort -> mkIte( + this@wideTo32BitsIfNeeded as UExpr, + mkBv(1, intSizeBits), + mkBv(0, intSizeBits) + ) + + is UBvSort -> { + if (sort.sizeBits < intSizeBits) { + (this@wideTo32BitsIfNeeded as UExpr).mkNarrow(intSizeBits.toInt(), signed) + } else { + this@wideTo32BitsIfNeeded as UExpr + } + } + + is UFpSort -> this@wideTo32BitsIfNeeded + else -> error("Unexpected sort: $sort") + } + } + +internal fun UExpr.mkNarrow(sizeBits: Int, signed: Boolean): UExpr { + val diff = sizeBits - sort.sizeBits.toInt() + val res = if (diff > 0) { + if (signed) { + ctx.mkBvSignExtensionExpr(diff, this) + } else { + ctx.mkBvZeroExtensionExpr(diff, this) + } + } else { + ctx.mkBvExtractExpr(high = sizeBits - 1, low = 0, this) + } + return res +} + +internal fun UExpr.castToBv(sizeBits: Int): UExpr = + with(ctx) { + val bvMaxValue = bvMaxValueSigned(sizeBits.toUInt()) + val bvMinValue = bvMinValueSigned(sizeBits.toUInt()) + val fpBvMaxValue = mkBvToFpExpr(sort, fpRoundingModeSortDefaultValue(), bvMaxValue, signed = true) + val fpBvMinValue = mkBvToFpExpr(sort, fpRoundingModeSortDefaultValue(), bvMinValue, signed = true) + + mkIte( + mkFpIsNaNExpr(this@castToBv), + mkBv(0, sizeBits.toUInt()), + mkIte( + mkFpLessExpr(fpBvMaxValue, this@castToBv), + bvMaxValue, + mkIte( + mkFpLessExpr(this@castToBv, fpBvMinValue), + bvMinValue, + mkFpToBvExpr( + mkFpRoundingModeExpr(io.ksmt.expr.KFpRoundingMode.RoundTowardZero), + this@castToBv, + sizeBits, + isSigned = true + ) + ) + ) + ) + } \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt new file mode 100644 index 0000000000..ef63694d46 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt @@ -0,0 +1,73 @@ +package org.usvm.operator + +import io.ksmt.expr.KExpr +import io.ksmt.utils.cast +import org.usvm.UBoolSort +import org.usvm.UBvSort +import org.usvm.UExpr +import org.usvm.UFpSort +import org.usvm.USort +import org.usvm.GoContext +import org.usvm.type.goCtx + +sealed class GoUnaryOperator( + val onBool: GoContext.(UExpr) -> UExpr = shouldNotBeCalled, + val onBv: GoContext.(UExpr) -> UExpr = shouldNotBeCalled, + val onFp: GoContext.(UExpr) -> UExpr = shouldNotBeCalled, +) { + data object Neg : GoUnaryOperator( + onBool = GoContext::mkNot, + onBv = GoContext::mkBvNegationExpr, + onFp = GoContext::mkFpNegationExpr, + ) + + data object CastToBool : GoUnaryOperator( + onBool = { it }, + onBv = { operand -> operand neq mkBv(0, operand.sort) } + ) + + data object CastToInt8 : GoUnaryOperator( + onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBv = { operand -> operand.mkNarrow(Byte.SIZE_BITS, signed = true) } + ) + + data object CastToInt16 : GoUnaryOperator( + onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBv = { operand -> operand.mkNarrow(Short.SIZE_BITS, signed = true) } + ) + + data object CastToInt32 : GoUnaryOperator( + onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBv = { operand -> operand.mkNarrow(Int.SIZE_BITS, signed = true) }, + onFp = { operand -> operand.castToBv(Int.SIZE_BITS) } + ) + + data object CastToInt64 : GoUnaryOperator( + onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBv = { operand -> operand.mkNarrow(Long.SIZE_BITS, signed = true) }, + onFp = { operand -> operand.castToBv(Long.SIZE_BITS) } + ) + + data object CastToFloat32 : GoUnaryOperator( + onBv = { operand -> mkBvToFpExpr(fp32Sort, fpRoundingModeSortDefaultValue(), operand, signed = true) }, + onFp = { operand -> mkFpToFpExpr(fp32Sort, fpRoundingModeSortDefaultValue(), operand) } + ) + + data object CastToFloat64 : GoUnaryOperator( + onBv = { operand -> mkBvToFpExpr(fp64Sort, fpRoundingModeSortDefaultValue(), operand, signed = true) }, + onFp = { operand -> mkFpToFpExpr(fp64Sort, fpRoundingModeSortDefaultValue(), operand) } + ) + + open operator fun invoke(operand: UExpr): UExpr = + when (operand.sort) { + is UBoolSort -> operand.goCtx.onBool(operand.cast()) + is UBvSort -> operand.goCtx.onBv(operand.cast()) + is UFpSort -> operand.goCtx.onFp(operand.cast()) + else -> error("Expressions mismatch: $operand") + } + + companion object { + private val shouldNotBeCalled: GoContext.(UExpr) -> KExpr = + { _ -> error("Should not be called") } + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt new file mode 100644 index 0000000000..aa2c41c257 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt @@ -0,0 +1,21 @@ +package org.usvm.state + +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoType +import org.usvm.UExpr +import org.usvm.USort + +sealed interface GoMethodResult { + data object NoCall : GoMethodResult + + class Success( + val value: UExpr, + val method: GoMethod, + val type: GoType, + ) : GoMethodResult + + class Panic( + val value: UExpr, + val type: GoType + ) : GoMethodResult +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt new file mode 100644 index 0000000000..d4a058d186 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -0,0 +1,401 @@ +package org.usvm.state + +import io.ksmt.expr.KBitVec32Value +import io.ksmt.expr.KConst +import io.ksmt.utils.asExpr +import io.ksmt.utils.cast +import org.jacodb.go.api.ArrayType +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.GoAllocExpr +import org.jacodb.go.api.GoAssignInst +import org.jacodb.go.api.GoFreeVar +import org.jacodb.go.api.GoFunction +import org.jacodb.go.api.GoInst +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoParameter +import org.jacodb.go.api.GoType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.PointerType +import org.jacodb.go.api.SliceType +import org.usvm.GoCall +import org.usvm.GoContext +import org.usvm.GoTarget +import org.usvm.PathNode +import org.usvm.UBoolSort +import org.usvm.UCallStack +import org.usvm.UConcreteHeapRef +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.ULValuePointer +import org.usvm.UNullRef +import org.usvm.USort +import org.usvm.UState +import org.usvm.api.allocateArray +import org.usvm.api.allocateArrayInitialized +import org.usvm.api.readField +import org.usvm.api.writeArrayLength +import org.usvm.api.writeField +import org.usvm.collection.field.UFieldLValue +import org.usvm.collections.immutable.internal.MutabilityOwnership +import org.usvm.constraints.UPathConstraints +import org.usvm.memory.ULValue +import org.usvm.memory.UMemory +import org.usvm.memory.URegisterStackLValue +import org.usvm.merging.MutableMergeGuard +import org.usvm.mkSizeExpr +import org.usvm.model.UModelBase +import org.usvm.sampleUValue +import org.usvm.sizeSort +import org.usvm.targets.UTargetsSet +import org.usvm.type.GoBasicTypes +import org.usvm.type.underlying + +class GoState( + ctx: GoContext, + ownership: MutabilityOwnership, + override val entrypoint: GoMethod, + callStack: UCallStack = UCallStack(), + pathConstraints: UPathConstraints = UPathConstraints(ctx, ownership), + memory: UMemory = UMemory(ctx, ownership, pathConstraints.typeConstraints), + models: List> = listOf(), + pathNode: PathNode = PathNode.root(), + forkPoints: PathNode> = PathNode.root(), + targets: UTargetsSet = UTargetsSet.empty(), + var methodResult: GoMethodResult = GoMethodResult.NoCall, + var data: GoStateData = GoStateData() +) : UState( + ctx, + ownership, + callStack, + pathConstraints, + memory, + models, + pathNode, + forkPoints, + targets +) { + override fun clone(newConstraints: UPathConstraints?): GoState { + val newThisOwnership = MutabilityOwnership() + val cloneOwnership = MutabilityOwnership() + val clonedConstraints = newConstraints?.also { + this.pathConstraints.changeOwnership(newThisOwnership) + it.changeOwnership(cloneOwnership) + } ?: pathConstraints.clone(newThisOwnership, cloneOwnership) + this.ownership = newThisOwnership + return GoState( + ctx, + ownership, + entrypoint, + callStack.clone(), + clonedConstraints, + memory.clone(clonedConstraints.typeConstraints, newThisOwnership, cloneOwnership), + models, + pathNode, + forkPoints, + targets.clone(), + methodResult, + data.clone() + ) + } + + override fun mergeWith(other: GoState, by: Unit): GoState? { + val newThisOwnership = MutabilityOwnership() + val newOtherOwnership = MutabilityOwnership() + val mergedOwnership = MutabilityOwnership() + + require(entrypoint == other.entrypoint) { "Cannot merge states with different entrypoints" } + // TODO: copy-paste + + val mergedPathNode = pathNode.mergeWith(other.pathNode, Unit) ?: return null + val mergedForkPoints = forkPoints.mergeWith(other.forkPoints, Unit) ?: return null + + val mergeGuard = MutableMergeGuard(ctx) + val mergedCallStack = callStack.mergeWith(other.callStack, Unit) ?: return null + val mergedPathConstraints = pathConstraints.mergeWith( + other.pathConstraints, mergeGuard, newThisOwnership, newOtherOwnership, mergedOwnership + ) ?: return null + val mergedMemory = + memory.clone(mergedPathConstraints.typeConstraints, newThisOwnership, newOtherOwnership) + .mergeWith(other.memory, mergeGuard, newThisOwnership, newOtherOwnership, mergedOwnership) + ?: return null + val mergedModels = models + other.models + val methodResult = if (other.methodResult == GoMethodResult.NoCall && methodResult == GoMethodResult.NoCall) { + GoMethodResult.NoCall + } else { + return null + } + val mergedTargets = targets.takeIf { it == other.targets } ?: return null + mergedPathConstraints += ctx.mkOr(mergeGuard.thisConstraint, mergeGuard.otherConstraint) + + val mergedData = data.mergeWith(other.data) + + return GoState( + ctx, + mergedOwnership, + entrypoint, + mergedCallStack, + mergedPathConstraints, + mergedMemory, + mergedModels, + mergedPathNode, + mergedForkPoints, + mergedTargets, + methodResult, + mergedData + ) + } + + override val isExceptional: Boolean get() = methodResult is GoMethodResult.Panic + + override fun toString(): String = buildString { + appendLine("Instruction: $currentStatement") + if (isExceptional) appendLine("Exception: $methodResult") + appendLine(callStack) + } + + fun newInst(inst: GoInst) { + pathNode += inst + } + + fun returnValue(valueToReturn: UExpr, type: GoType) { + val returnFromMethod = lastEnteredMethod + val returnSite = callStack.pop() + if (callStack.isNotEmpty()) { + memory.stack.pop() + } + + if (!isExceptional) { + methodResult = GoMethodResult.Success(valueToReturn, returnFromMethod, type) + } + + if (returnSite != null) { + newInst(returnSite) + } + + data.flowStack.removeLast() + } + + fun handlePanic() { + require(methodResult is GoMethodResult.Panic) + + val returnSite = callStack.pop() + if (callStack.isNotEmpty()) { + memory.stack.pop() + } + + if (returnSite != null) { + newInst(returnSite) + } + } + + fun panic(expr: UExpr, type: GoType) { + methodResult = GoMethodResult.Panic(expr.cast(), type) + data.flowStack.add(GoFlowStatus.PANIC) + } + + fun panic(text: String) = panic(mkString(text), GoBasicTypes.STRING) + + fun recover(): UExpr = with(ctx) { + if (methodResult is GoMethodResult.Panic) { + val result = (methodResult as GoMethodResult.Panic).value + methodResult = GoMethodResult.NoCall + return result + } + return nullRef + } + + fun runDefers() { + data.flowStack.add(GoFlowStatus.DEFER) + } + + fun addCall(call: GoCall, returnInst: GoInst? = null) = with(ctx) { + val methodInfo = getMethodInfo(call.method) + val freeVariables = mutableListOf>().also { + if (call.method is GoFunction) { + call.method.freeVars.forEach { variable -> it.add(findParam(variable)) } + } + } + val parameters = mutableListOf().also { + if (call.method is GoFunction) { + it.addAll(call.method.parameters) + } + } + + data.flowStack.add(GoFlowStatus.NORMAL) + callStack.push(call.method, returnInst) + if (methodInfo.arguments.isEmpty()) { + memory.stack.push(methodInfo.argumentsCount, methodInfo.variablesCount) + } else { + memory.stack.push(methodInfo.arguments, methodInfo.variablesCount) + } + + parameters.forEachIndexed { i, parameter -> + when (val type = parameter.type) { + is ArrayType -> { + val ref = memory.read(URegisterStackLValue(addressSort, i)).asExpr(addressSort) + memory.writeArrayLength(ref, mkSizeExpr(type.len.toInt()), type, sizeSort) + } + } + } + + freeVariables.forEachIndexed { i, variable -> + val lvalue = URegisterStackLValue(variable.sort, i + freeVariableOffset(call.method)) + memory.write(lvalue, variable.asExpr(variable.sort), trueExpr) + } + + newInst(call.entrypoint) + } + + fun deref(pointer: UHeapRef, sort: USort): UExpr { + val lvaluePointer = tryLValuePointer(pointer) + if (lvaluePointer != null) { + return memory.read(lvaluePointer.lvalue) + } + + val index = 0 + return memory.readField(pointer, index, sort) + } + + fun store(pointer: UHeapRef, rvalue: UExpr) { + val lvaluePointer = tryLValuePointer(pointer) + if (lvaluePointer != null) { + memory.write(lvaluePointer.lvalue.withSort(rvalue.sort), rvalue.asExpr(rvalue.sort), ctx.trueExpr) + return + } + + val index = 0 + memory.writeField(pointer, index, rvalue.sort, rvalue.asExpr(rvalue.sort), ctx.trueExpr) + } + + fun isPointer(pointer: UHeapRef): UExpr { + val index = 1 + val field = memory.readField(pointer, index, ctx.bv32Sort) + return ctx.mkEq(field, ctx.mkBv(POINTER_FIELD, ctx.bv32Sort)) + } + + fun isBoxed(ref: UHeapRef): UExpr { + val index = 1 + val field = memory.readField(ref, index, ctx.bv32Sort) + return ctx.mkEq(field, ctx.mkBv(BOXED_VALUE_FIELD, ctx.bv32Sort)) + } + + fun isBoxedConcrete(ref: UHeapRef): Boolean { + val index = 1 + val field = memory.readField(ref, index, ctx.bv32Sort) + return field is KBitVec32Value && field.intValue == BOXED_VALUE_FIELD + } + + fun box(expr: UExpr, targetType: GoType): UHeapRef { + if (expr.sort == ctx.addressSort && expr !is UNullRef && expr !is KConst<*> && isBoxedConcrete(expr.asExpr(ctx.addressSort))) { + return box(unbox(expr.asExpr(ctx.addressSort), ctx.typeToSort(targetType.underlying())), targetType) + } + + return mkTuple(targetType, expr, ctx.mkBv(BOXED_VALUE_FIELD, ctx.bv32Sort)) + } + + fun unbox(expr: UHeapRef, sort: USort): UExpr { + val index = 0 + return memory.readField(expr, index, sort) + } + + fun mkPointer(type: GoType): UConcreteHeapRef { + return mkPointer(type, sampleValue(type)) + } + + fun mkPointer(type: GoType, expr: UExpr): UConcreteHeapRef { + return mkTuple(type, expr, ctx.mkBv(POINTER_FIELD, ctx.bv32Sort)) + } + + fun mkPointer(type: GoType, lvalue: ULValue<*, *>): UExpr { + return mkPointer(type, ULValuePointer(ctx, lvalue)) + } + + fun mkTuple(type: GoType, vararg fields: UExpr): UConcreteHeapRef = with(ctx) { + val ref = memory.allocConcrete(type) + for ((index, field) in fields.withIndex()) { + memory.write(UFieldLValue(field.sort, ref, index), field.asExpr(field.sort), trueExpr) + } + return ref + } + + fun mkString(value: String): UExpr { + return memory.allocateArrayInitialized( + GoBasicTypes.STRING, + ctx.bv8Sort, + ctx.sizeSort, + value.toByteArray().map { ctx.mkBv(it) }.asSequence() + ) + } + + private fun sampleValue(type: GoType): UExpr = when (type) { + is ArrayType -> { + val sort = ctx.typeToSort(type.elementType) + val contents = Array(type.len.toInt()) { sampleValue(type.elementType).asExpr(sort) }.asSequence() + memory.allocateArrayInitialized(type, sort, ctx.sizeSort, contents) + } + + is SliceType -> memory.allocateArray(type, ctx.sizeSort, ctx.mkSizeExpr(0)) + is BasicType -> ctx.typeToSort(type).sampleUValue() + is NamedType -> box(sampleValue(type.underlyingType), type) + else -> memory.allocConcrete(type) + } + + private fun tryLValuePointer(pointer: UHeapRef): ULValuePointer? { + val index = 0 + val payload = memory.readField(pointer, index, ctx.addressSort) + if (payload is ULValuePointer) { + return payload + } + return null + } + + private fun findParam(freeVar: GoFreeVar): UExpr { + val stack = callStack.clone() + val registers = memory.stack.clone() + while (!stack.isEmpty()) { + val param = findParam(stack.lastMethod(), freeVar) + if (param != null) { + return registers.read(URegisterStackLValue(ctx.typeToSort(param.first), param.second)) + } + + stack.pop() + registers.pop() + } + + + throw IllegalStateException("param not found") + } + + private fun findParam(method: GoMethod, freeVar: GoFreeVar): Pair? { + val param = method.parameters.filterIsInstance().find { it.name == freeVar.name } + if (param != null) { + return Pair(PointerType(param.type), param.index + ctx.localVariableOffset(method)) + } + + val assign = method.blocks.flatMap { it.instructions }.filterIsInstance().find { + it.rhv is GoAllocExpr && (it.rhv as GoAllocExpr).comment == freeVar.name + } + if (assign != null) { + val alloc = assign.rhv as GoAllocExpr + return Pair(PointerType(alloc.type), index(method, alloc.name)) + } + + return null + } + + private fun index(method: GoMethod, name: String): Int { + return name.substring(1).toInt() + ctx.localVariableOffset(method) + } + + private fun ULValue<*, *>.withSort(sort: T): ULValue<*, T> { + check(this@withSort.sort == sort) { "Sort mismatch" } + + @Suppress("UNCHECKED_CAST") + return this@withSort as ULValue<*, T> + } + + companion object { + const val POINTER_FIELD = 50541 + const val BOXED_VALUE_FIELD = 80085 + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt new file mode 100644 index 0000000000..613516b9a9 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt @@ -0,0 +1,42 @@ +package org.usvm.state + +import org.jacodb.go.api.GoMethod +import org.usvm.GoCall + +enum class GoFlowStatus { + NORMAL, + PANIC, + DEFER, +} + +class GoStateData( + val flowStack: ArrayDeque = ArrayDeque(), +) { + private val deferredCalls: MutableMap> = hashMapOf() + + val flowStatus: GoFlowStatus + get() = flowStack.last() + + fun getDeferredCalls(method: GoMethod): ArrayDeque = deferredCalls[method] ?: ArrayDeque() + + fun addDeferredCall(method: GoMethod, call: GoCall) { + if (method !in deferredCalls) { + deferredCalls[method] = ArrayDeque() + } + deferredCalls[method]!!.addLast(call) + } + + fun clone(): GoStateData = GoStateData(clonedFlowStack()).mergeWith(this) + + fun mergeWith(other: GoStateData) = GoStateData(clonedFlowStack()).also { + for (entry in other.deferredCalls) { + entry.value.forEach { c -> it.addDeferredCall(entry.key, c) } + } + } + + private fun clonedFlowStack(): ArrayDeque { + val newStack = ArrayDeque() + newStack.addAll(flowStack) + return newStack + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt new file mode 100644 index 0000000000..a97ca4808e --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt @@ -0,0 +1,92 @@ +package org.usvm.type + +import io.ksmt.KAst +import io.ksmt.cache.hash +import io.ksmt.cache.structurallyEqual +import io.ksmt.expr.printer.ExpressionPrinter +import io.ksmt.expr.transformer.KTransformerBase +import io.ksmt.sort.KSortVisitor +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.GoType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.SliceType +import org.usvm.GoContext +import org.usvm.UExpr +import org.usvm.USort + +object GoTypes { + const val BOOL = "bool" + const val INT = "int" + const val INT8 = "int8" + const val INT16 = "int16" + const val INT32 = "int32" + const val INT64 = "int64" + const val UINT = "uint" + const val UINT8 = "uint8" + const val UINT16 = "uint16" + const val UINT32 = "uint32" + const val UINT64 = "uint64" + const val FLOAT32 = "float32" + const val FLOAT64 = "float64" + const val STRING = "string" + const val BYTE = "byte" + const val RUNE = "rune" + + const val UINTPTR = "uintptr" + const val UNTYPED_BOOL = "untyped bool" + const val UNTYPED_INT = "untyped int" + const val UNTYPED_RUNE = "untyped rune" + const val UNTYPED_FLOAT = "untyped float" + const val UNTYPED_STRING = "untyped string" + + const val UNSAFE_POINTER = "unsafe.Pointer" +} + +object GoBasicTypes { + val BOOL = BasicType(GoTypes.BOOL) + val INT = BasicType(GoTypes.INT) + val INT8 = BasicType(GoTypes.INT8) + val INT16 = BasicType(GoTypes.INT16) + val INT32 = BasicType(GoTypes.INT32) + val INT64 = BasicType(GoTypes.INT64) + val UINT = BasicType(GoTypes.UINT) + val UINT8 = BasicType(GoTypes.UINT8) + val UINT16 = BasicType(GoTypes.UINT16) + val UINT32 = BasicType(GoTypes.UINT32) + val UINT64 = BasicType(GoTypes.UINT64) + val UINTPTR = BasicType(GoTypes.UINTPTR) + val FLOAT32 = BasicType(GoTypes.FLOAT32) + val FLOAT64 = BasicType(GoTypes.FLOAT64) + val STRING = SliceType(UINT8) + val RUNE = BasicType(GoTypes.RUNE) + val UNSAFE_POINTER = BasicType(GoTypes.UNSAFE_POINTER) +} + +fun GoType.underlying(): GoType = when (this) { + is NamedType -> this.underlyingType.underlying() + else -> this +} + +class GoVoidSort(ctx: GoContext) : USort(ctx) { + override fun print(builder: StringBuilder) { + builder.append("void sort") + } + + override fun accept(visitor: KSortVisitor): T = error("should not be called") +} + +class GoVoidValue(ctx: GoContext) : UExpr(ctx) { + override val sort: GoVoidSort get() = goCtx.voidSort + + override fun internEquals(other: Any): Boolean = structurallyEqual(other) + + override fun internHashCode(): Int = hash() + + override fun accept(transformer: KTransformerBase): GoVoidValue = this + + override fun print(printer: ExpressionPrinter) { + printer.append("void") + } +} + +val KAst.goCtx get() = ctx as GoContext \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt new file mode 100644 index 0000000000..37f019fa0c --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt @@ -0,0 +1,67 @@ +package org.usvm.type + +import org.jacodb.go.api.ArrayType +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.GoType +import org.jacodb.go.api.InterfaceType +import org.jacodb.go.api.MapType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.PointerType +import org.jacodb.go.api.SignatureType +import org.jacodb.go.api.SliceType +import org.jacodb.go.api.StructType +import org.jacodb.go.api.TupleType +import org.usvm.types.USupportTypeStream +import org.usvm.types.UTypeStream +import org.usvm.types.UTypeSystem +import kotlin.time.Duration + +class GoTypeSystem( + override val typeOperationsTimeout: Duration, + val types: Collection +) : UTypeSystem { + private val goAnyType = InterfaceType(emptyList(), "any") + private val topTypeStream by lazy { USupportTypeStream.from(this, goAnyType) } + + override fun topTypeStream(): UTypeStream { + return topTypeStream + } + + override fun findSubtypes(type: GoType): Sequence { + return types.asSequence().filter { isSupertype(type, it) } + } + + override fun isInstantiable(type: GoType): Boolean = when (type) { + is StructType, is MapType, is SliceType, is ArrayType, is BasicType -> true + is NamedType -> isInstantiable(type.underlyingType) + is PointerType -> isInstantiable(type.baseType) + else -> false + } + + override fun isFinal(type: GoType): Boolean = when (type) { + is BasicType, is ArrayType, is SliceType, is MapType, is PointerType, is SignatureType, is TupleType -> true + else -> false + } + + override fun hasCommonSubtype(type: GoType, types: Collection): Boolean = when (type) { + is BasicType, is ArrayType, is SliceType, is MapType, is PointerType, is SignatureType, is TupleType -> types.isEmpty() + is InterfaceType -> types.none { !isFinal(it) } + is NamedType -> hasCommonSubtype(type.underlyingType, types) + is StructType -> types.all { it is InterfaceType || isSupertype(it, type) } + else -> false + } + + override fun isSupertype(supertype: GoType, type: GoType): Boolean = when { + supertype == type -> true + supertype is NamedType && supertype.underlyingType is InterfaceType -> isSupertype(supertype.underlyingType, type) + supertype is InterfaceType -> implements(supertype, type) + else -> false + } + + private fun implements(iface: InterfaceType, impl: GoType): Boolean = when { + iface.methods.isEmpty() -> true + impl is NamedType -> impl.methods.containsAll(iface.methods) + impl is PointerType -> implements(iface, impl.baseType) + else -> false + } +} \ No newline at end of file diff --git a/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt b/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt new file mode 100644 index 0000000000..f92baa671d --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt @@ -0,0 +1,28 @@ +package org.usvm.util + +import org.jacodb.go.api.GoInstLocation +import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoNullInst +import java.nio.ByteBuffer + +val ByteBuffer.bool: Boolean + get() = this.get() == 1.toByte() + +val ByteBuffer.byte: Byte + get() = this.get() + +fun GoMethod.hasUnsupportedInstructions(): Boolean { + return blocks.any { it.instructions.any { inst -> inst is GoNullInst } } +} + +fun GoMethod.isInit(location: GoInstLocation): Boolean { + return isInit() && packageName == location.method.packageName +} + +fun GoMethod.isInit(): Boolean { + return name.matches("^init$".toRegex()) // ^init(#\d+)?$ for all inits is not needed because init#1, init#2 and others are called in init() +} + +fun GoMethod.isOsInit(): Boolean { + return name.matches("^osinit$".toRegex()) +} diff --git a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt new file mode 100644 index 0000000000..d8b4f7ef79 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt @@ -0,0 +1,104 @@ +package org.usvm + +import org.jacodb.go.api.GoMethod +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.usvm.model.Converter +import org.usvm.model.Parser +import java.nio.file.Files +import java.nio.file.Paths +import java.util.stream.Collectors +import kotlin.io.path.extension +import kotlin.io.path.pathString +import kotlin.system.measureTimeMillis +import kotlin.time.Duration + +class JacoDbTest { + @TestFactory + fun jacodbTestLong(): Collection { + val pkg = Converter.unpack(Parser().deserialize("out/usvm_examples.json")) + val program = GoProgram(listOf(pkg)) + val machine = GoMachine(program, options, customOptions) + return methods(pkg).filter { it.metName in longMethods }.map { + DynamicTest.dynamicTest(it.metName) { + println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + } + } + } + + @TestFactory + fun jacodbTestFast(): Collection { + val pkg = Converter.unpack(Parser().deserialize("out/usvm_examples.json")) + val program = GoProgram(listOf(pkg)) + val machine = GoMachine(program, options, customOptions) + return methods(pkg) + .filter { it.metName !in longMethods } + .let { if (whitelist.isEmpty()) it else it.filter { m -> m.metName in whitelist } } + .map { + DynamicTest.dynamicTest(it.metName) { + println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + } + } + } + + @TestFactory + fun jacodbTestImports(): Collection { + val packages = Files.list(Paths.get("out")) + .filter { p -> p.extension != "yaml" } + .map { Converter.unpack(Parser().deserialize(it.pathString)) } + .collect(Collectors.toList()) + val program = GoProgram(packages) + val machine = GoMachine(program, options, customOptions) + val pkg = program.findPackage("usvm/examples/imports") + return methods(pkg).filter { it.metName !in longMethods }.map { + DynamicTest.dynamicTest(it.metName) { + println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + } + } + } + + @TestFactory + fun jacodbTestStdStrings(): Collection { + val packages = Files.list(Paths.get("out")) + .filter { p -> p.extension != "yaml" } + .map { Converter.unpack(Parser().deserialize(it.pathString)) } + .collect(Collectors.toList()) + val program = GoProgram(packages) + val machine = GoMachine(program, options, customOptions) + val pkg = program.findPackage("flag") + return methods(pkg).filter { it.metName !in longMethods }.map { + DynamicTest.dynamicTest(it.metName) { + println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + } + } + } + + private fun methods(pkg: GoPackage): List = pkg.methods.filter { !it.metName.contains('$') } + + private val options: UMachineOptions = UMachineOptions( + pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), + coverageZone = CoverageZone.TRANSITIVE, + exceptionsPropagation = true, +// timeout = 5_000.milliseconds, + timeout = Duration.INFINITE, // for debug + stepsFromLastCovered = 1000000L, + solverTimeout = Duration.INFINITE, // we do not need the timeout for a solver in tests + typeOperationsTimeout = Duration.INFINITE, // we do not need the timeout for type operations in tests + ) + + private val customOptions: GoMachineOptions = GoMachineOptions( + failOnNotFullCoverage = false, + uncoveredMethods = listOf("panicRecoverComplex") + ) + + private val longMethods = arrayOf("loopInfinite", "loopInner", "loopCollatz", "mapLoopLen", "canVisitAllRooms") + + private val whitelist = arrayOf( +// "sliceCustomAppend" +// "appendErrorStrings" +// "(usvm/examples.Person).WithName" +// "sliceCustomAppend" +// "shiftErrorToString" +// "mapCustomDeleteSimple" + ) +} \ No newline at end of file diff --git a/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt b/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt new file mode 100644 index 0000000000..a6344ca960 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt @@ -0,0 +1,21 @@ +package org.usvm.model + +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.test.assertEquals + +class ModelTest { + @Test + fun testEqualContent() { + val filename = "out/usvm_examples.json" + val file = File(filename) + + val parser = Parser() + val pkg = parser.deserialize(filename) + + val expected = file.readText() + val actual = parser.serialize(pkg) + + assertEquals(expected, actual) + } +} \ No newline at end of file diff --git a/usvm-go/src/test/resources/logback.xml b/usvm-go/src/test/resources/logback.xml new file mode 100644 index 0000000000..cf11c9cba0 --- /dev/null +++ b/usvm-go/src/test/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d{HH:mm:ss.SSS} |%.-1level| %replace(%c{0}){'(\$Companion)?\$logger\$1',''} - %msg%n + + + + + + + \ No newline at end of file diff --git a/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt b/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt index 2d48201b4f..7075521821 100644 --- a/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt +++ b/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt @@ -132,7 +132,7 @@ class WeightedAaTree(private val comparator: Comparator) { } } - private tailrec fun removeRec(value: T, deleteFrom: AaTreeNode?, k: (AaTreeNode?) -> AaTreeNode?): AaTreeNode? { + private fun removeRec(value: T, deleteFrom: AaTreeNode?, k: (AaTreeNode?) -> AaTreeNode?): AaTreeNode? { if (deleteFrom == null) { return null } From b84931bfb96347792863e4288366431c654eeb22 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Mon, 12 Oct 2026 16:11:11 +0300 Subject: [PATCH 2/9] Adapt Go prototype to current build APIs --- build.gradle.kts | 1 + .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 4 +-- usvm-go/src/main/kotlin/org/usvm/GoMachine.kt | 2 +- .../org/usvm/memory/GoArrayAllocation.kt | 29 +++++++++++++++++++ .../src/main/kotlin/org/usvm/state/GoState.kt | 6 ++-- 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 usvm-go/src/main/kotlin/org/usvm/memory/GoArrayAllocation.kt diff --git a/build.gradle.kts b/build.gradle.kts index 181c4fb92e..6087557380 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ tasks.register("validateProjectList") { // Define the expected subprojects here. val expectedProjects = setOf( project(":usvm-core"), + project(":usvm-go"), project(":usvm-detekt-rules"), project(":usvm-util"), project(":usvm-dataflow"), diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index 13013c7e9e..d78ff4992d 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -91,7 +91,6 @@ import org.usvm.api.UnknownBinaryOperationException import org.usvm.api.UnknownFunctionException import org.usvm.api.UnknownUnaryOperationException import org.usvm.api.UnsupportedUnaryOperationException -import org.usvm.api.allocateArray import org.usvm.api.collection.ObjectMapCollectionApi.ensureObjectMapSizeCorrect import org.usvm.api.collection.ObjectMapCollectionApi.mkSymbolicObjectMap import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapAnyKey @@ -131,6 +130,7 @@ import org.usvm.type.GoBasicTypes import org.usvm.type.underlying import org.usvm.types.first import org.usvm.util.isInit +import org.usvm.memory.allocateGoArray as allocateArray class GoExprVisitor( private val ctx: GoContext, @@ -1079,4 +1079,4 @@ class GoExprVisitor( private fun unsupportedExpr(name: String): UExpr { throw UnsupportedOperationException("Expression '$name' not supported") } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt index 9fbdc7e51a..8092020a00 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt @@ -35,7 +35,7 @@ val logger = object : KLogging() {}.logger class GoMachine( private val program: GoProgram, - private val options: UMachineOptions, + override val options: UMachineOptions, private val customOptions: GoMachineOptions ) : UMachine() { private val typeSystem = GoTypeSystem(options.typeOperationsTimeout, program.types.values) diff --git a/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayAllocation.kt b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayAllocation.kt new file mode 100644 index 0000000000..279e57f377 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayAllocation.kt @@ -0,0 +1,29 @@ +package org.usvm.memory + +import org.jacodb.go.api.GoType +import org.usvm.UConcreteHeapRef +import org.usvm.UExpr +import org.usvm.USort +import org.usvm.api.initializeArray +import org.usvm.api.initializeArrayLength + +internal fun UWritableMemory.allocateGoArray( + type: GoType, + sizeSort: SizeSort, + count: UExpr, +): UConcreteHeapRef { + val reference = allocConcrete(type) + initializeArrayLength(reference, type, sizeSort, count) + return reference +} + +internal fun UWritableMemory.allocateGoArrayInitialized( + type: GoType, + sort: Sort, + sizeSort: SizeSort, + contents: Sequence>, +): UConcreteHeapRef { + val reference = allocConcrete(type) + initializeArray(reference, type, sort, sizeSort, contents) + return reference +} diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt index d4a058d186..a88faad6a5 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -30,8 +30,6 @@ import org.usvm.ULValuePointer import org.usvm.UNullRef import org.usvm.USort import org.usvm.UState -import org.usvm.api.allocateArray -import org.usvm.api.allocateArrayInitialized import org.usvm.api.readField import org.usvm.api.writeArrayLength import org.usvm.api.writeField @@ -49,6 +47,8 @@ import org.usvm.sizeSort import org.usvm.targets.UTargetsSet import org.usvm.type.GoBasicTypes import org.usvm.type.underlying +import org.usvm.memory.allocateGoArray as allocateArray +import org.usvm.memory.allocateGoArrayInitialized as allocateArrayInitialized class GoState( ctx: GoContext, @@ -398,4 +398,4 @@ class GoState( const val POINTER_FIELD = 50541 const val BOXED_VALUE_FIELD = 80085 } -} \ No newline at end of file +} From 2a5b1d0db0610d2b25129232c7ec625231aed05d Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Tue, 13 Oct 2026 01:46:41 +0300 Subject: [PATCH 3/9] Normalize named scalar map keys across storage and resolution --- .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 24 +++++++++++++------ .../src/main/kotlin/org/usvm/GoInstVisitor.kt | 2 +- .../org/usvm/interpreter/GoTestInterpreter.kt | 10 ++++---- .../main/kotlin/org/usvm/type/GoMapKeyType.kt | 12 ++++++++++ 4 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 usvm-go/src/main/kotlin/org/usvm/type/GoMapKeyType.kt diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index d78ff4992d..a8e52f3e97 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -127,6 +127,7 @@ import org.usvm.state.GoMethodResult import org.usvm.state.GoState.Companion.POINTER_FIELD import org.usvm.statistics.ApplicationGraph import org.usvm.type.GoBasicTypes +import org.usvm.type.mapKeyStorageType import org.usvm.type.underlying import org.usvm.types.first import org.usvm.util.isInit @@ -455,7 +456,7 @@ class GoExprVisitor( override fun visitGoLookupExpr(expr: GoLookupExpr): UExpr { val map = unboxNamedRef(expr.instance.accept(this).asExpr(ctx.addressSort), expr.instance.type) val mapType = expr.instance.type.underlying() as MapType - val key = expr.index.accept(this) + val key = resolveMapKey(expr.index) val isRefKey = key.sort == ctx.addressSort val commaOk = expr.commaOk @@ -535,9 +536,10 @@ class GoExprVisitor( val length = symbolicObjectMapSize(collection, collectionType) val ok = ctx.mkAnd(notNull, ctx.mkBvSignedGreaterExpr(length, ctx.mkBv(0))) - val isPrimitiveKey = collectionType.keyType.underlying() is BasicType + val keySort = ctx.typeToSort(collectionType.keyType.mapKeyStorageType()) + val isPrimitiveKey = keySort != ctx.addressSort val (key, value) = if (isPrimitiveKey) { - val k = symbolicPrimitiveMapAnyKey(collection, collectionType, ctx.typeToSort(collectionType.keyType), USizeExprKeyInfo()) + val k = symbolicPrimitiveMapAnyKey(collection, collectionType, keySort, USizeExprKeyInfo()) val v = symbolicPrimitiveMapGet(collection, k, collectionType, ctx.typeToSort(collectionType.valueType), USizeExprKeyInfo()) symbolicPrimitiveMapRemove(collection, k, collectionType, USizeExprKeyInfo()) k to v @@ -550,7 +552,7 @@ class GoExprVisitor( mkTuple( TupleType(listOf(GoBasicTypes.BOOL, collectionType.keyType, collectionType.valueType)), - ok, key, value + ok, if (isPrimitiveKey) tryBox(key, collectionType.keyType) else key, value ) } @@ -843,6 +845,14 @@ class GoExprVisitor( return unbox(expr.asExpr(ctx.addressSort), ctx.typeToSort(type.underlying())) } + internal fun resolveMapKey(value: GoValue): UExpr { + val key = value.accept(this) + val storageType = value.type.mapKeyStorageType() + if (storageType == value.type) return key + + return unboxNamedPrimitive(key, value.type) + } + private fun tryBox(expr: UExpr, targetType: GoType): UExpr { return if (targetType is NamedType) box(expr, targetType) else expr } @@ -907,9 +917,9 @@ class GoExprVisitor( "delete" -> { val map = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) - val key = args[1].accept(this) + val key = resolveMapKey(args[1]) val mapType = args[0].type.underlying() as MapType - val keySort = ctx.typeToSort(mapType.keyType) + val keySort = ctx.typeToSort(mapType.keyType.mapKeyStorageType()) scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw IllegalStateException() scope.doWithState { @@ -975,7 +985,7 @@ class GoExprVisitor( checkNotNull(srcMap) ?: throw IllegalStateException() scope.ensureObjectMapSizeCorrect(srcMap, mapType) ?: throw IllegalStateException() - val keySort = typeToSort(mapType.keyType) + val keySort = typeToSort(mapType.keyType.mapKeyStorageType()) val valueSort = typeToSort(mapType.valueType) val isRefSet = keySort == addressSort return scope.calcOnState { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt index 81f8391f93..a474672035 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -131,7 +131,7 @@ class GoInstVisitor( override fun visitGoMapUpdateInst(inst: GoMapUpdateInst): GoInst { val map = exprVisitor.unboxNamedRef(inst.map.accept(exprVisitor).asExpr(ctx.addressSort), inst.map.type) val type = inst.map.type.underlying() - val key = inst.key.accept(exprVisitor) + val key = exprVisitor.resolveMapKey(inst.key) val value = inst.value.accept(exprVisitor) val isRefKey = key.sort == ctx.addressSort diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt index 68aa1436c7..83acb17545 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -59,6 +59,7 @@ import org.usvm.state.GoMethodResult import org.usvm.state.GoState import org.usvm.type.GoBasicTypes import org.usvm.type.GoVoidSort +import org.usvm.type.mapKeyStorageType import org.usvm.type.underlying import org.usvm.types.first import java.nio.ByteBuffer @@ -204,7 +205,8 @@ class GoTestInterpreter( return null } - val keySort = typeToSort(keyType) + val keyStorageType = keyType.mapKeyStorageType() + val keySort = typeToSort(keyStorageType) val valueSort = typeToSort(valueType) val isRefSet = keySort == addressSort @@ -223,7 +225,7 @@ class GoTestInterpreter( UMapEntryLValue(keySort, valueSort, map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) } val value = memory.read(lvalue) - convertExpr(key, keyType) to convertExpr(value, valueType) + convertExpr(key, keyStorageType) to convertExpr(value, valueType) }) } val getEntries: (UHeapRef) -> Set> = { @@ -242,12 +244,12 @@ class GoTestInterpreter( if (length > result.size) { val diff = length - result.size - val rng = RNG(keyType as BasicType) + val rng = if (isRefSet) null else RNG(keyStorageType as BasicType) for (i in 0 until diff) { val key = if (isRefSet) { convertExpr(state.symbolicObjectMapAnyKey(map, mapType), keyType) } else { - rng.generateUniqueMapKey(result) + requireNotNull(rng).generateUniqueMapKey(result) } val value = convertExpr(valueSort.sampleUValue(), valueType) result[key] = value diff --git a/usvm-go/src/main/kotlin/org/usvm/type/GoMapKeyType.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoMapKeyType.kt new file mode 100644 index 0000000000..3102de09a9 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoMapKeyType.kt @@ -0,0 +1,12 @@ +package org.usvm.type + +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.GoType + +/** Named numeric and boolean keys use their scalar payload, not their box identity. */ +internal fun GoType.mapKeyStorageType(): GoType { + val underlying = underlying() + val isScalar = underlying is BasicType && underlying != GoBasicTypes.STRING + + return if (isScalar) underlying else this +} From 13e7935aead2ad2f89299aeed4b0b17a1084ab4c Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Tue, 13 Oct 2026 01:46:41 +0300 Subject: [PATCH 4/9] Preserve variadic method identity in Go SSA exports --- usvm-go/src/main/go/pack.go | 13 ++++++-- usvm-go/src/main/go/pack_test.go | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 usvm-go/src/main/go/pack_test.go diff --git a/usvm-go/src/main/go/pack.go b/usvm-go/src/main/go/pack.go index 29541e1b62..bbcaed6c0f 100644 --- a/usvm-go/src/main/go/pack.go +++ b/usvm-go/src/main/go/pack.go @@ -645,14 +645,21 @@ func (p *Package) PackMethods(in WithMethods) []string { for i := 0; i < in.NumMethods(); i++ { method := in.Method(i) signature := method.Type().(*types.Signature) - methods = append(methods, method.Name()+p.PackMethodParams(signature.Params())+p.PackMethodResults(signature.Results())) + methods = append(methods, method.Name()+p.PackMethodParams(signature)+p.PackMethodResults(signature.Results())) } return methods } -func (p *Package) PackMethodParams(in *types.Tuple) string { - return "(" + strings.Join(p.PackTupleTypes(in), ", ") + ")" +func (p *Package) PackMethodParams(signature *types.Signature) string { + params := p.PackTupleTypes(signature.Params()) + if signature.Variadic() { + last := len(params) - 1 + slice := signature.Params().At(last).Type().(*types.Slice) + params[last] = "..." + slice.Elem().String() + } + + return "(" + strings.Join(params, ", ") + ")" } func (p *Package) PackMethodResults(in *types.Tuple) string { diff --git a/usvm-go/src/main/go/pack_test.go b/usvm-go/src/main/go/pack_test.go new file mode 100644 index 0000000000..6f1dd259b1 --- /dev/null +++ b/usvm-go/src/main/go/pack_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" +) + +func TestMethodSignaturesPreserveVariadicParameters(t *testing.T) { + const source = `package signatures + type Variadic interface { Count(...int) int } + type Slice interface { Count([]int) int } + type SliceReceiver struct{} + func (SliceReceiver) Count(values []int) int { return len(values) } + type VariadicReceiver struct{} + func (VariadicReceiver) Count(values ...int) int { return len(values) } + ` + files := token.NewFileSet() + file, err := parser.ParseFile(files, "signatures.go", source, 0) + if err != nil { + t.Fatal(err) + } + pkg, err := new(types.Config).Check("signatures", files, []*ast.File{file}, nil) + if err != nil { + t.Fatal(err) + } + variadic := pkg.Scope().Lookup("Variadic").Type().Underlying().(*types.Interface) + slice := pkg.Scope().Lookup("Slice").Type().Underlying().(*types.Interface) + sliceReceiver := pkg.Scope().Lookup("SliceReceiver").Type() + variadicReceiver := pkg.Scope().Lookup("VariadicReceiver").Type() + exporter := new(Package) + + cases := []struct { + name string + receiver types.Type + contract *types.Interface + }{ + {"slice versus variadic", sliceReceiver, variadic}, + {"variadic versus slice", variadicReceiver, slice}, + {"matching slice", sliceReceiver, slice}, + {"matching variadic", variadicReceiver, variadic}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + actual := exporter.PackMethods(test.receiver)[0] == exporter.PackMethods(test.contract)[0] + expected := types.Implements(test.receiver, test.contract) + + if actual != expected { + t.Fatalf("exported method identity agrees=%v, Go interface implementation=%v", actual, expected) + } + }) + } +} From bb6a69056442035aeab39c5ff77059eba66593cf Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Tue, 13 Oct 2026 03:02:45 +0300 Subject: [PATCH 5/9] Use named receiver types in Go method export regression --- usvm-go/src/main/go/pack_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/usvm-go/src/main/go/pack_test.go b/usvm-go/src/main/go/pack_test.go index 6f1dd259b1..7342f3696a 100644 --- a/usvm-go/src/main/go/pack_test.go +++ b/usvm-go/src/main/go/pack_test.go @@ -28,13 +28,13 @@ func TestMethodSignaturesPreserveVariadicParameters(t *testing.T) { } variadic := pkg.Scope().Lookup("Variadic").Type().Underlying().(*types.Interface) slice := pkg.Scope().Lookup("Slice").Type().Underlying().(*types.Interface) - sliceReceiver := pkg.Scope().Lookup("SliceReceiver").Type() - variadicReceiver := pkg.Scope().Lookup("VariadicReceiver").Type() + sliceReceiver := pkg.Scope().Lookup("SliceReceiver").Type().(*types.Named) + variadicReceiver := pkg.Scope().Lookup("VariadicReceiver").Type().(*types.Named) exporter := new(Package) cases := []struct { name string - receiver types.Type + receiver *types.Named contract *types.Interface }{ {"slice versus variadic", sliceReceiver, variadic}, From cf96e0dcfaddf67418450b167954eac8d08d7682 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Tue, 13 Oct 2026 06:28:30 +0300 Subject: [PATCH 6/9] [Go] Refresh prototype API and source hygiene --- buildSrc/src/main/kotlin/Dependencies.kt | 2 +- .../kotlin/org/usvm/GoApplicationGraph.kt | 2 +- .../src/main/kotlin/org/usvm/GoComponents.kt | 2 +- usvm-go/src/main/kotlin/org/usvm/GoContext.kt | 2 +- usvm-go/src/main/kotlin/org/usvm/GoDomain.kt | 4 +- .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 85 +++++++++--- .../src/main/kotlin/org/usvm/GoInstVisitor.kt | 9 +- usvm-go/src/main/kotlin/org/usvm/GoMachine.kt | 19 ++- .../main/kotlin/org/usvm/GoMachineOptions.kt | 4 +- usvm-go/src/main/kotlin/org/usvm/GoTarget.kt | 2 +- .../org/usvm/interpreter/GoTestInterpreter.kt | 27 ++-- .../main/kotlin/org/usvm/model/Converter.kt | 128 ++++++++++++------ .../src/main/kotlin/org/usvm/model/Model.kt | 8 +- .../src/main/kotlin/org/usvm/model/Parser.kt | 2 +- .../org/usvm/operator/GoBinaryOperator.kt | 2 +- .../org/usvm/operator/GoOperatorUtils.kt | 2 +- .../org/usvm/operator/GoUnaryOperator.kt | 12 +- .../kotlin/org/usvm/state/GoMethodResult.kt | 4 +- .../src/main/kotlin/org/usvm/state/GoState.kt | 10 +- .../main/kotlin/org/usvm/state/GoStateData.kt | 2 +- .../src/main/kotlin/org/usvm/type/GoType.kt | 2 +- .../main/kotlin/org/usvm/type/GoTypeSystem.kt | 12 +- .../main/kotlin/org/usvm/util/Extensions.kt | 5 +- .../src/test/kotlin/org/usvm/JacoDbTest.kt | 40 ++++-- .../test/kotlin/org/usvm/model/ModelTest.kt | 2 +- 25 files changed, 263 insertions(+), 126 deletions(-) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 753c5d7955..32faeb8eb7 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -120,7 +120,7 @@ object Libs { val jacodb_go = dep( group = jacodbPackage, name = "jacodb-go", - version = "816194b963" + version = "a839f86ca8" ) val jacodb_core = dep( diff --git a/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt b/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt index 0d464367b0..4c978cb972 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt @@ -50,4 +50,4 @@ class GoApplicationGraph : ApplicationGraph { override fun statementsOf(method: GoMethod): Sequence { return method.flowGraph().instructions.asSequence() } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt b/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt index 937ceb762d..edde50f1a2 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt @@ -32,4 +32,4 @@ class GoComponents( override fun > mkSizeExprProvider(ctx: Context): USizeExprProvider { return UBv32SizeExprProvider(ctx) } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoContext.kt b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt index 39c77ba23b..c73f995fb7 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoContext.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt @@ -91,4 +91,4 @@ class GoContext( GoBasicTypes.FLOAT64 -> fp64Sort else -> addressSort } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt index d9b5dda5a6..91858151ab 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt @@ -13,7 +13,7 @@ import org.usvm.util.isOsInit class GoMethodInfo( val variablesCount: Int, val argumentsCount: Int, - val arguments: Array> + val arguments: Array>, ) { override fun toString(): String { return "variables: $variablesCount, arguments: $argumentsCount" @@ -29,7 +29,7 @@ class GoPackage( val name: String, val methods: List, val globals: List, - val types: Map + val types: Map, ) { private val methodsIndex: Map = methods.associateBy { it.metName } diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index a8e52f3e97..65aac1f39c 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -170,7 +170,8 @@ class GoExprVisitor( func is GoFunction -> program.findMethod(expr.location, func.metName) func is GoVar -> scope.calcOnState { - program.findMethod(expr.location, (memory.read(URegisterStackLValue(ctx.addressSort, index(func.name))) as KConst).decl.name) + val functionRef = memory.read(URegisterStackLValue(ctx.addressSort, index(func.name))) as KConst + program.findMethod(expr.location, functionRef.decl.name) } else -> throw UnknownFunctionException(func.toString()) @@ -320,7 +321,14 @@ class GoExprVisitor( for (i in 0 until arrayType.len) { val idx = ctx.mkSizeExpr(i.toInt()) val element = memory.readArrayIndex(slice, idx, sliceType, ctx.typeToSort(sliceType.elementType)) - memory.writeArrayIndex(array, idx, arrayType, ctx.typeToSort(arrayType.elementType), element, ctx.trueExpr) + memory.writeArrayIndex( + array, + idx, + arrayType, + ctx.typeToSort(arrayType.elementType), + element, + ctx.trueExpr, + ) } tryBox(mkPointer(arrayType, array), expr.type) @@ -370,12 +378,15 @@ class GoExprVisitor( override fun visitGoSliceExpr(expr: GoSliceExpr): UExpr { val boxedArray = expr.array.accept(this).asExpr(ctx.addressSort) - val array = unboxNamedRef(boxedArray, expr.array.type).let { if (isPointerConcrete(it)) deref(it, ctx.addressSort) else it } + val unboxedArray = unboxNamedRef(boxedArray, expr.array.type) + val array = if (isPointerConcrete(unboxedArray)) deref(unboxedArray, ctx.addressSort) else unboxedArray val arrayType = expr.array.type.let { if (it is PointerType) it.baseType else it }.underlying() val sliceType = expr.type.underlying() val length = scope.calcOnState { memory.readArrayLength(array, arrayType, ctx.sizeSort) } val low = expr.low.accept(this).asExpr(ctx.sizeSort) - val high = expr.high.accept(this).let { if (it is KBitVec32Value && it.intValue == -1) length else it }.asExpr(ctx.sizeSort) + val suppliedHigh = expr.high.accept(this) + val hasDefaultHigh = suppliedHigh is KBitVec32Value && suppliedHigh.intValue == -1 + val high = (if (hasDefaultHigh) length else suppliedHigh).asExpr(ctx.sizeSort) val count = ctx.mkSizeSubExpr(high, low) val elementType = when (arrayType) { is ArrayType -> arrayType.elementType @@ -504,7 +515,8 @@ class GoExprVisitor( return scope.calcOnState { mkTuple( TupleType(listOf(expr.instance.type, GoBasicTypes.INT32)), - collection, ctx.mkSizeExpr(0) + collection, + ctx.mkSizeExpr(0), ) } } @@ -527,7 +539,9 @@ class GoExprVisitor( memory.writeField(iter, 1, ctx.sizeSort, ctx.mkBvAddExpr(index, ctx.mkSizeExpr(1)), ctx.trueExpr) mkTuple( TupleType(listOf(GoBasicTypes.BOOL, GoBasicTypes.INT32, GoBasicTypes.INT32)), - ok, index, ctx.mkPrimitiveCast(char, ctx.bv32Sort) + ok, + index, + ctx.mkPrimitiveCast(char, ctx.bv32Sort), ) } @@ -540,19 +554,32 @@ class GoExprVisitor( val isPrimitiveKey = keySort != ctx.addressSort val (key, value) = if (isPrimitiveKey) { val k = symbolicPrimitiveMapAnyKey(collection, collectionType, keySort, USizeExprKeyInfo()) - val v = symbolicPrimitiveMapGet(collection, k, collectionType, ctx.typeToSort(collectionType.valueType), USizeExprKeyInfo()) + val v = symbolicPrimitiveMapGet( + collection, + k, + collectionType, + ctx.typeToSort(collectionType.valueType), + USizeExprKeyInfo(), + ) symbolicPrimitiveMapRemove(collection, k, collectionType, USizeExprKeyInfo()) k to v } else { val k = symbolicObjectMapAnyKey(collection, collectionType) - val v = symbolicObjectMapGet(collection, k, collectionType, ctx.typeToSort(collectionType.valueType)) + val v = symbolicObjectMapGet( + collection, + k, + collectionType, + ctx.typeToSort(collectionType.valueType), + ) symbolicObjectMapRemove(collection, k, collectionType) k to v } mkTuple( TupleType(listOf(GoBasicTypes.BOOL, collectionType.keyType, collectionType.valueType)), - ok, if (isPrimitiveKey) tryBox(key, collectionType.keyType) else key, value + ok, + if (isPrimitiveKey) tryBox(key, collectionType.keyType) else key, + value, ) } @@ -585,7 +612,11 @@ class GoExprVisitor( val unboxedValue = unbox(xAddr, assertSort) return scope.calcOnState { - val sample = if (assertSort == ctx.addressSort) ctx.nullRef else assertSort.sampleUValue().asExpr(assertSort) + val sample = if (assertSort == ctx.addressSort) { + ctx.nullRef + } else { + assertSort.sampleUValue().asExpr(assertSort) + } if (commaOk) { mkTuple(tupleType, ite(xAddr, unboxedValue, sample), ite(xAddr, ctx.trueExpr, ctx.falseExpr)) } else { @@ -610,7 +641,8 @@ class GoExprVisitor( override fun visitGoFreeVar(expr: GoFreeVar): UExpr { return scope.calcOnState { - memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), expr.index + ctx.freeVariableOffset(lastEnteredMethod))) + val registerIndex = expr.index + ctx.freeVariableOffset(lastEnteredMethod) + memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), registerIndex)) } } @@ -715,7 +747,9 @@ class GoExprVisitor( } private fun visitGoBinaryExpr(expr: GoBinaryExpr): UExpr { - if (expr.lhv.type.underlying() == GoBasicTypes.STRING && expr.rhv.type.underlying() == GoBasicTypes.STRING) { + val bothStrings = expr.lhv.type.underlying() == GoBasicTypes.STRING && + expr.rhv.type.underlying() == GoBasicTypes.STRING + if (bothStrings) { return tryBox(appendArray(expr.lhv, expr.rhv), expr.type) } @@ -763,9 +797,11 @@ class GoExprVisitor( } private fun visitIndexExpr(instance: GoValue, idx: GoValue): Pair> { - val array = unboxNamedRef(instance.accept(this).asExpr(ctx.addressSort), instance.type).let { if (isPointerConcrete(it)) deref(it, ctx.addressSort) else it } + val unboxedArray = unboxNamedRef(instance.accept(this).asExpr(ctx.addressSort), instance.type) + val array = if (isPointerConcrete(unboxedArray)) deref(unboxedArray, ctx.addressSort) else unboxedArray val arrayType = instance.type.let { if (it is PointerType) it.baseType else it }.underlying() - val index = bv(unboxNamedPrimitive(idx.accept(this), idx.type)).mkNarrow(Int.SIZE_BITS, false).asExpr(ctx.sizeSort) + val indexValue = bv(unboxNamedPrimitive(idx.accept(this), idx.type)) + val index = indexValue.mkNarrow(Int.SIZE_BITS, signed = false).asExpr(ctx.sizeSort) val length = scope.calcOnState { memory.readArrayLength(array, arrayType, ctx.sizeSort) } checkLength(length) ?: throw IllegalStateException() @@ -776,9 +812,17 @@ class GoExprVisitor( return array to index } - private fun normalize(lhs: UExpr, rhs: UExpr, goExpr: GoBinaryExpr, signed: Boolean): UExpr = with(ctx) { + private fun normalize( + lhs: UExpr, + rhs: UExpr, + goExpr: GoBinaryExpr, + signed: Boolean, + ): UExpr = with(ctx) { when (goExpr) { - is GoShrExpr -> if (bv(lhs).sort.sizeBits != bv(rhs).sort.sizeBits) bv(rhs).mkNarrow(Long.SIZE_BITS, signed).asExpr(bv64Sort) else rhs + is GoShrExpr -> { + val differentWidths = bv(lhs).sort.sizeBits != bv(rhs).sort.sizeBits + if (differentWidths) bv(rhs).mkNarrow(Long.SIZE_BITS, signed).asExpr(bv64Sort) else rhs + } else -> rhs } } @@ -1004,7 +1048,11 @@ class GoExprVisitor( val append = unboxNamedRef(appendValue.accept(this).asExpr(ctx.addressSort), appendValue.type) val sliceType = sliceValue.type.underlying() - val elementSort = if (sliceType == GoBasicTypes.STRING) ctx.bv8Sort else ctx.typeToSort((sliceType as SliceType).elementType) + val elementSort = if (sliceType == GoBasicTypes.STRING) { + ctx.bv8Sort + } else { + ctx.typeToSort((sliceType as SliceType).elementType) + } val zero = ctx.mkSizeExpr(0) return scope.calcOnState { @@ -1075,7 +1123,8 @@ class GoExprVisitor( val method = GoFunction(signature, emptyList(), funcName, emptyList(), "", emptyList(), emptyList()) val mockSort = ctx.typeToSort(returnType) val mockValue = scope.calcOnState { - memory.mocker.call(method, expr.args.map { it.accept(this@GoExprVisitor) }.asSequence(), mockSort, memory.ownership) + val arguments = expr.args.map { it.accept(this@GoExprVisitor) }.asSequence() + memory.mocker.call(method, arguments, mockSort, memory.ownership) } if (mockSort == ctx.addressSort) { val constraint = scope.calcOnState { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt index a474672035..2151a1ce32 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -67,7 +67,8 @@ class GoInstVisitor( 1 -> returnValue(inst.returnValues[0].accept(exprVisitor), inst.returnValues[0].type) else -> { val type = TupleType(inst.returnValues.map { it.type }) - returnValue(mkTuple(type, fields = inst.returnValues.map { it.accept(exprVisitor) }.toTypedArray()), type) + val fields = inst.returnValues.map { it.accept(exprVisitor) }.toTypedArray() + returnValue(mkTuple(type, fields = fields), type) } } } @@ -155,7 +156,11 @@ class GoInstVisitor( memory.write(mapContainsLValue, ctx.trueExpr, ctx.trueExpr) val updatedSize = ctx.mkSizeAddExpr(currentSize, ctx.mkSizeExpr(1)) - memory.write(UMapLengthLValue(map, type, ctx.sizeSort), updatedSize, ctx.mkNot(memory.read(mapContainsLValue))) + memory.write( + UMapLengthLValue(map, type, ctx.sizeSort), + updatedSize, + ctx.mkNot(memory.read(mapContainsLValue)), + ) } return next(inst) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt index 8092020a00..a44bcd15ea 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt @@ -36,7 +36,7 @@ val logger = object : KLogging() {}.logger class GoMachine( private val program: GoProgram, override val options: UMachineOptions, - private val customOptions: GoMachineOptions + private val customOptions: GoMachineOptions, ) : UMachine() { private val typeSystem = GoTypeSystem(options.typeOperationsTimeout, program.types.values) private val applicationGraph = GoApplicationGraph() @@ -66,7 +66,7 @@ class GoMachine( logger.debug("{}.analyze()", this) val excludedMethods = methodsList.filter { it.hasUnsupportedInstructions() }.map { it.metName } if (excludedMethods.isNotEmpty()) { - logger.warn("The following methods will be skipped due to having unsupported instructions: {}", excludedMethods) + logger.warn { "Skipping methods with unsupported instructions: $excludedMethods" } } val methods = methodsList.filter { !it.hasUnsupportedInstructions() } @@ -151,15 +151,20 @@ class GoMachine( stopStrategy = stopStrategy, ) - println("Total coverage: ${coverageStatistics.getTotalCoverage().roundToInt()}%") + val totalCoverage = coverageStatistics.getTotalCoverage().roundToInt() + logger.info { "Total coverage: $totalCoverage%" } for (method in coverageStatistics.coverageZone) { - println("Method ${method.metName} coverage: ${coverageStatistics.getMethodCoverage(method).roundToInt()}%") + val methodCoverage = coverageStatistics.getMethodCoverage(method).roundToInt() + logger.info { "Method ${method.metName} coverage: $methodCoverage%" } } - if (coverageStatistics.getTotalCoverage().roundToInt() < 100 && !customOptions.uncoveredMethods.containsAll(methods.map { it.metName })) { + val analyzedMethods = methods.map { it.metName } + val hasUnexpectedUncoveredMethods = totalCoverage < FULL_COVERAGE_PERCENT && + !customOptions.uncoveredMethods.containsAll(analyzedMethods) + if (hasUnexpectedUncoveredMethods) { if (customOptions.failOnNotFullCoverage) { throw IllegalStateException("coverage not 100%") } else { - logger.warn("analysis of methods ${methods.map { it.metName }} lead to coverage < 100%") + logger.warn { "Analysis of methods $analyzedMethods led to coverage < 100%" } } } @@ -170,3 +175,5 @@ class GoMachine( return state.callStack.isEmpty() } } + +private const val FULL_COVERAGE_PERCENT = 100 diff --git a/usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt b/usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt index 4f2c4349eb..583cde1f23 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachineOptions.kt @@ -2,5 +2,5 @@ package org.usvm data class GoMachineOptions( val failOnNotFullCoverage: Boolean, - val uncoveredMethods: List -) \ No newline at end of file + val uncoveredMethods: List, +) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt b/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt index 7f7e64f65a..8a6291e2f9 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt @@ -5,4 +5,4 @@ import org.usvm.targets.UTarget abstract class GoTarget( location: GoInst, -) : UTarget(location) \ No newline at end of file +) : UTarget(location) diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt index 83acb17545..25d406da0c 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -119,9 +119,13 @@ class GoTestInterpreter( prefix + when (val type = baseType.underlying()) { GoBasicTypes.STRING -> if (named) resolveBoxed(h, type) else resolveString(h, type) is BasicType -> resolveBoxed(h, type) - is ArrayType -> if (named) resolveBoxed(h, type) else resolveArray(h, type, type.len, type.elementType) + is ArrayType -> { + if (named) resolveBoxed(h, type) else resolveArray(h, type, type.len, type.elementType) + } is SliceType -> if (named) resolveBoxed(h, type) else resolveSlice(h, type, type.elementType) - is MapType -> if (named) resolveBoxed(h, type) else resolveMap(h, type, type.keyType, type.valueType) + is MapType -> { + if (named) resolveBoxed(h, type) else resolveMap(h, type, type.keyType, type.valueType) + } is TupleType -> resolveTuple(h, type) is StructType -> if (named) resolveBoxed(h, type) else resolveStruct(h, type) is InterfaceType -> resolveInterface(h) @@ -200,7 +204,12 @@ class GoTestInterpreter( } } - fun resolveMap(map: UHeapRef, mapType: GoType, keyType: GoType, valueType: GoType): Map? = with(ctx) { + fun resolveMap( + map: UHeapRef, + mapType: GoType, + keyType: GoType, + valueType: GoType, + ): Map? = with(ctx) { if (map == mkConcreteHeapRef(NULL_ADDRESS) || map == nullRef) { return null } @@ -276,7 +285,9 @@ class GoTestInterpreter( } return structType.fields?.mapIndexed { idx, type -> idx to type }?.associate { - Pair("field${it.first}", convertExpr(memory.readField(struct, it.first, typeToSort(it.second)), it.second)) + val (index, fieldType) = it + val fieldValue = memory.readField(struct, index, typeToSort(fieldType)) + Pair("field$index", convertExpr(fieldValue, fieldType)) } } @@ -333,7 +344,7 @@ class GoTestInterpreter( sealed interface ProgramExecutionResult class InputModel( - private val arguments: List + private val arguments: List, ) { override fun toString(): String { return buildString { @@ -345,7 +356,7 @@ class InputModel( } class OutputModel( - private val returnExpr: Any? + private val returnExpr: Any?, ) { override fun toString(): String { return buildString { @@ -358,7 +369,7 @@ class OutputModel( class SuccessfulExecutionResult( private val inputModel: InputModel, - private val outputModel: OutputModel + private val outputModel: OutputModel, ) : ProgramExecutionResult { override fun toString(): String { return buildString { @@ -393,7 +404,7 @@ class UnsuccessfulExecutionResult( class RNG( type: BasicType, seed: Long = 0, - private val maxAttempts: Long = 10 + private val maxAttempts: Long = 10, ) { private val random = Random(seed) diff --git a/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt index ffd288e1fe..dd013d1d55 100644 --- a/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt @@ -110,7 +110,10 @@ object Converter { typesMap.putAll(pkg.types.mapValues { type -> unpack(pkg.types, type.value) }) val methods = pkg.members.filterIsInstance().map { function -> GoFunction( - SignatureType(TupleType(function.parameters.map { getType(it.goType) }), TupleType(function.returnTypes.map(::getType))), + SignatureType( + TupleType(function.parameters.map { getType(it.goType) }), + TupleType(function.returnTypes.map(::getType)), + ), function.parameters.mapIndexed(this::unpack), function.name, emptyList(), @@ -144,31 +147,64 @@ object Converter { is Instruction.Alloc -> GoAllocExpr(loc, getType(inst.goType), inst.register, inst.comment).toAssignInst() is Instruction.BinOp -> unpack(loc, inst) is Instruction.Call -> unpack(loc, inst) - is Instruction.ChangeInterface -> GoChangeInterfaceExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() - is Instruction.ChangeType -> GoChangeTypeExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() - is Instruction.Convert -> GoConvertExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + is Instruction.ChangeInterface -> { + GoChangeInterfaceExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + } + is Instruction.ChangeType -> { + GoChangeTypeExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + } + is Instruction.Convert -> { + GoConvertExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + } is Instruction.DebugRef -> unsupportedInstruction(function) is Instruction.Defer -> GoDeferInst(loc, unpack(inst.value), inst.args.map(::unpack)) is Instruction.Extract -> unpack(loc, inst) - is Instruction.Field -> GoFieldExpr(loc, getType(inst.goType), unpack(inst.struct), inst.field, inst.register).toAssignInst() - is Instruction.FieldAddr -> GoFieldAddrExpr(loc, getType(inst.goType), unpack(inst.struct), inst.field, inst.register).toAssignInst() + is Instruction.Field -> { + GoFieldExpr(loc, getType(inst.goType), unpack(inst.struct), inst.field, inst.register).toAssignInst() + } + is Instruction.FieldAddr -> { + GoFieldAddrExpr( + loc, + getType(inst.goType), + unpack(inst.struct), + inst.field, + inst.register, + ).toAssignInst() + } is Instruction.Go -> unsupportedInstruction(function) - is Instruction.If -> GoIfInst(loc, unpack(inst.condition) as GoConditionExpr, GoInstRef(inst.trueBranch), GoInstRef(inst.falseBranch)) + is Instruction.If -> { + GoIfInst( + loc, + unpack(inst.condition) as GoConditionExpr, + GoInstRef(inst.trueBranch), + GoInstRef(inst.falseBranch), + ) + } is Instruction.Index -> unpack(loc, inst) is Instruction.IndexAddr -> unpack(loc, inst) is Instruction.Jump -> GoJumpInst(loc, GoInstRef(inst.index)) is Instruction.Lookup -> unpack(loc, inst) is Instruction.MakeChan -> unsupportedInstruction(function) is Instruction.MakeClosure -> unpack(loc, inst) - is Instruction.MakeInterface -> GoMakeInterfaceExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() - is Instruction.MakeMap -> GoMakeMapExpr(loc, getType(inst.goType), unpack(inst.reserve), inst.register).toAssignInst() + is Instruction.MakeInterface -> { + GoMakeInterfaceExpr(loc, getType(inst.goType), unpack(inst.value), inst.register).toAssignInst() + } + is Instruction.MakeMap -> { + GoMakeMapExpr(loc, getType(inst.goType), unpack(inst.reserve), inst.register).toAssignInst() + } is Instruction.MakeSlice -> unpack(loc, inst) is Instruction.MapUpdate -> GoMapUpdateInst(loc, unpack(inst.map), unpack(inst.key), unpack(inst.value)) is Instruction.MultiConvert -> unsupportedInstruction(function) - is Instruction.Next -> GoNextExpr(loc, getType(inst.goType), unpack(inst.iter), inst.register).toAssignInst() + is Instruction.Next -> { + GoNextExpr(loc, getType(inst.goType), unpack(inst.iter), inst.register).toAssignInst() + } is Instruction.Panic -> GoPanicInst(loc, unpack(inst.value)) - is Instruction.Phi -> GoPhiExpr(loc, getType(inst.goType), inst.edges.map(this::unpack), inst.register).toAssignInst() - is Instruction.Range -> GoRangeExpr(loc, getType(inst.goType), unpack(inst.collection), inst.register).toAssignInst() + is Instruction.Phi -> { + GoPhiExpr(loc, getType(inst.goType), inst.edges.map(this::unpack), inst.register).toAssignInst() + } + is Instruction.Range -> { + GoRangeExpr(loc, getType(inst.goType), unpack(inst.collection), inst.register).toAssignInst() + } is Instruction.Return -> GoReturnInst(loc, inst.results.map(this::unpack)) is Instruction.RunDefers -> GoRunDefersInst(loc) is Instruction.Select -> unsupportedInstruction(function) @@ -182,7 +218,7 @@ object Converter { } private fun unpack(location: GoInstLocation, binOp: Instruction.BinOp): GoInst { - val binaryExprConstructors = mapOf>( + val constructors = mapOf>( "&&" to ::GoAndExpr, "&^" to ::GoAndNotExpr, "||" to ::GoOrExpr, @@ -202,8 +238,8 @@ object Converter { ">>" to ::GoShrExpr, ) - if (binaryExprConstructors.containsKey(binOp.op)) { - return binaryExprConstructors[binOp.op]!!( + if (constructors.containsKey(binOp.op)) { + return constructors[binOp.op]!!( location, getType(binOp.goType), unpack(binOp.first), @@ -227,7 +263,13 @@ object Converter { } private fun unpack(location: GoInstLocation, extract: Instruction.Extract): GoInst { - return GoExtractExpr(location, getType(extract.goType), unpack(extract.tuple), extract.index, extract.register).toAssignInst() + return GoExtractExpr( + location, + getType(extract.goType), + unpack(extract.tuple), + extract.index, + extract.register, + ).toAssignInst() } private fun unpack(location: GoInstLocation, index: Instruction.Index): GoInst { @@ -338,7 +380,9 @@ object Converter { val name = unOp.register return when { - unaryExprConstructors.containsKey(unOp.op) -> unaryExprConstructors[unOp.op]!!(location, type, arg, name).toAssignInst() + unaryExprConstructors.containsKey(unOp.op) -> { + unaryExprConstructors[unOp.op]!!(location, type, arg, name).toAssignInst() + } unOp.op == "<-" -> GoUnArrowExpr(location, type, arg, unOp.commaOk, name).toAssignInst() else -> GoNullInst(location.method) } @@ -380,29 +424,33 @@ object Converter { return types.map { unpack(typesMap, it) } } - private fun unpack(types: Map, type: Type): GoType = internType(when (type) { - is Type.Alias -> unpack(types, type.from) - is Type.Array -> ArrayType(type.len, unpack(types, type.elem)) - is Type.Basic -> unpack(type.name) - is Type.Chan -> ChanType(type.dir.toLong(), unpack(types, type.elem)) - is Type.Interface -> InterfaceType(type.methods, type.name) - is Type.Map -> MapType(unpack(types, type.key), unpack(types, type.elem)) - is Type.Named -> { - val named = NamedType(NullType(), type.name, type.methods) - typesMap[type.name] = named - named.underlyingType = unpack(types, type.underlying) - named - } + private fun unpack(types: Map, type: Type): GoType = internType( + when (type) { + is Type.Alias -> unpack(types, type.from) + is Type.Array -> ArrayType(type.len, unpack(types, type.elem)) + is Type.Basic -> unpack(type.name) + is Type.Chan -> ChanType(type.dir.toLong(), unpack(types, type.elem)) + is Type.Interface -> InterfaceType(type.methods, type.name) + is Type.Map -> MapType(unpack(types, type.key), unpack(types, type.elem)) + is Type.Named -> { + val named = NamedType(NullType(), type.name, type.methods) + typesMap[type.name] = named + named.underlyingType = unpack(types, type.underlying) + named + } - is Type.Opaque -> OpaqueType(type.name) - is Type.Pointer -> PointerType(unpack(types, type.elem)) - is Type.Signature -> SignatureType(TupleType(unpack(types, type.params)), TupleType(unpack(types, type.results))) - is Type.Slice -> SliceType(unpack(types, type.elem)) - is Type.Struct -> StructType(unpack(types, type.fields), null) - is Type.Tuple -> TupleType(unpack(types, type.elems)) - is Type.TypeParam -> TypeParam(type.name) - is Type.Union -> UnionType(emptyList()) // TODO(buraindo) proper type list when generics are supported - }) + is Type.Opaque -> OpaqueType(type.name) + is Type.Pointer -> PointerType(unpack(types, type.elem)) + is Type.Signature -> { + SignatureType(TupleType(unpack(types, type.params)), TupleType(unpack(types, type.results))) + } + is Type.Slice -> SliceType(unpack(types, type.elem)) + is Type.Struct -> StructType(unpack(types, type.fields), null) + is Type.Tuple -> TupleType(unpack(types, type.elems)) + is Type.TypeParam -> TypeParam(type.name) + is Type.Union -> UnionType(emptyList()) // TODO(buraindo) proper type list when generics are supported + }, + ) private fun unpack(types: Map, type: String): GoType { if (type in typesMap) { @@ -461,4 +509,4 @@ object Converter { return s } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/model/Model.kt b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt index ab136fa5cc..ce4d561b92 100644 --- a/usvm-go/src/main/kotlin/org/usvm/model/Model.kt +++ b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable data class Package( val name: String, val members: List, - val types: Map + val types: Map, ) @Serializable @@ -618,14 +618,14 @@ sealed interface Type { @SerialName("Struct") data class Struct( override val name: String, - val fields: List + val fields: List, ) : Type @Serializable @SerialName("Tuple") data class Tuple( override val name: String, - val elems: List + val elems: List, ) : Type @Serializable @@ -639,4 +639,4 @@ sealed interface Type { data class Union( override val name: String, ) : Type -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/model/Parser.kt b/usvm-go/src/main/kotlin/org/usvm/model/Parser.kt index a918027316..e74c575095 100644 --- a/usvm-go/src/main/kotlin/org/usvm/model/Parser.kt +++ b/usvm-go/src/main/kotlin/org/usvm/model/Parser.kt @@ -95,4 +95,4 @@ class Parser { fun serialize(pkg: Package): String { return jsonSerializer.encodeToString(pkg) } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt index 2d4f72226d..420d3bed30 100644 --- a/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt @@ -2,13 +2,13 @@ package org.usvm.operator import io.ksmt.utils.asExpr import io.ksmt.utils.cast +import org.usvm.GoContext import org.usvm.UAddressSort import org.usvm.UBoolSort import org.usvm.UBvSort import org.usvm.UExpr import org.usvm.UFpSort import org.usvm.USort -import org.usvm.GoContext import org.usvm.type.goCtx import org.usvm.uctx diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt index 83afc6f727..bb251e9bfd 100644 --- a/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt @@ -73,4 +73,4 @@ internal fun UExpr.castToBv(sizeBits: Int): UExpr = ) ) ) - } \ No newline at end of file + } diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt index ef63694d46..2d6ff382fb 100644 --- a/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt @@ -2,12 +2,12 @@ package org.usvm.operator import io.ksmt.expr.KExpr import io.ksmt.utils.cast +import org.usvm.GoContext import org.usvm.UBoolSort import org.usvm.UBvSort import org.usvm.UExpr import org.usvm.UFpSort import org.usvm.USort -import org.usvm.GoContext import org.usvm.type.goCtx sealed class GoUnaryOperator( @@ -27,23 +27,23 @@ sealed class GoUnaryOperator( ) data object CastToInt8 : GoUnaryOperator( - onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBool = { operand -> operand.wideTo32BitsIfNeeded(signed = false) }, onBv = { operand -> operand.mkNarrow(Byte.SIZE_BITS, signed = true) } ) data object CastToInt16 : GoUnaryOperator( - onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBool = { operand -> operand.wideTo32BitsIfNeeded(signed = false) }, onBv = { operand -> operand.mkNarrow(Short.SIZE_BITS, signed = true) } ) data object CastToInt32 : GoUnaryOperator( - onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBool = { operand -> operand.wideTo32BitsIfNeeded(signed = false) }, onBv = { operand -> operand.mkNarrow(Int.SIZE_BITS, signed = true) }, onFp = { operand -> operand.castToBv(Int.SIZE_BITS) } ) data object CastToInt64 : GoUnaryOperator( - onBool = { operand -> operand.wideTo32BitsIfNeeded(false) }, + onBool = { operand -> operand.wideTo32BitsIfNeeded(signed = false) }, onBv = { operand -> operand.mkNarrow(Long.SIZE_BITS, signed = true) }, onFp = { operand -> operand.castToBv(Long.SIZE_BITS) } ) @@ -70,4 +70,4 @@ sealed class GoUnaryOperator( private val shouldNotBeCalled: GoContext.(UExpr) -> KExpr = { _ -> error("Should not be called") } } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt index aa2c41c257..da983c2035 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoMethodResult.kt @@ -16,6 +16,6 @@ sealed interface GoMethodResult { class Panic( val value: UExpr, - val type: GoType + val type: GoType, ) : GoMethodResult -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt index a88faad6a5..937ffe4185 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -62,7 +62,7 @@ class GoState( forkPoints: PathNode> = PathNode.root(), targets: UTargetsSet = UTargetsSet.empty(), var methodResult: GoMethodResult = GoMethodResult.NoCall, - var data: GoStateData = GoStateData() + var data: GoStateData = GoStateData(), ) : UState( ctx, ownership, @@ -286,7 +286,9 @@ class GoState( } fun box(expr: UExpr, targetType: GoType): UHeapRef { - if (expr.sort == ctx.addressSort && expr !is UNullRef && expr !is KConst<*> && isBoxedConcrete(expr.asExpr(ctx.addressSort))) { + val alreadyBoxed = expr.sort == ctx.addressSort && expr !is UNullRef && + expr !is KConst<*> && isBoxedConcrete(expr.asExpr(ctx.addressSort)) + if (alreadyBoxed) { return box(unbox(expr.asExpr(ctx.addressSort), ctx.typeToSort(targetType.underlying())), targetType) } @@ -395,7 +397,7 @@ class GoState( } companion object { - const val POINTER_FIELD = 50541 - const val BOXED_VALUE_FIELD = 80085 + const val POINTER_FIELD = 50_541 + const val BOXED_VALUE_FIELD = 80_085 } } diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt index 613516b9a9..944c327eec 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt @@ -39,4 +39,4 @@ class GoStateData( newStack.addAll(flowStack) return newStack } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt index a97ca4808e..4177c70701 100644 --- a/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt @@ -89,4 +89,4 @@ class GoVoidValue(ctx: GoContext) : UExpr(ctx) { } } -val KAst.goCtx get() = ctx as GoContext \ No newline at end of file +val KAst.goCtx get() = ctx as GoContext diff --git a/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt index 37f019fa0c..8930ef63ef 100644 --- a/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt @@ -18,7 +18,7 @@ import kotlin.time.Duration class GoTypeSystem( override val typeOperationsTimeout: Duration, - val types: Collection + val types: Collection, ) : UTypeSystem { private val goAnyType = InterfaceType(emptyList(), "any") private val topTypeStream by lazy { USupportTypeStream.from(this, goAnyType) } @@ -44,7 +44,9 @@ class GoTypeSystem( } override fun hasCommonSubtype(type: GoType, types: Collection): Boolean = when (type) { - is BasicType, is ArrayType, is SliceType, is MapType, is PointerType, is SignatureType, is TupleType -> types.isEmpty() + is BasicType, is ArrayType, is SliceType, is MapType, is PointerType, is SignatureType, is TupleType -> { + types.isEmpty() + } is InterfaceType -> types.none { !isFinal(it) } is NamedType -> hasCommonSubtype(type.underlyingType, types) is StructType -> types.all { it is InterfaceType || isSupertype(it, type) } @@ -53,7 +55,9 @@ class GoTypeSystem( override fun isSupertype(supertype: GoType, type: GoType): Boolean = when { supertype == type -> true - supertype is NamedType && supertype.underlyingType is InterfaceType -> isSupertype(supertype.underlyingType, type) + supertype is NamedType && supertype.underlyingType is InterfaceType -> { + isSupertype(supertype.underlyingType, type) + } supertype is InterfaceType -> implements(supertype, type) else -> false } @@ -64,4 +68,4 @@ class GoTypeSystem( impl is PointerType -> implements(iface, impl.baseType) else -> false } -} \ No newline at end of file +} diff --git a/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt b/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt index f92baa671d..81d14b2b17 100644 --- a/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt +++ b/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt @@ -20,9 +20,10 @@ fun GoMethod.isInit(location: GoInstLocation): Boolean { } fun GoMethod.isInit(): Boolean { - return name.matches("^init$".toRegex()) // ^init(#\d+)?$ for all inits is not needed because init#1, init#2 and others are called in init() + // Synthetic init#N methods are invoked by the package init method. + return name == "init" } fun GoMethod.isOsInit(): Boolean { - return name.matches("^osinit$".toRegex()) + return name == "osinit" } diff --git a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt index d8b4f7ef79..e1640fcac3 100644 --- a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt @@ -21,7 +21,7 @@ class JacoDbTest { val machine = GoMachine(program, options, customOptions) return methods(pkg).filter { it.metName in longMethods }.map { DynamicTest.dynamicTest(it.metName) { - println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + analyzeAndLog(machine, pkg, it.metName) } } } @@ -36,43 +36,53 @@ class JacoDbTest { .let { if (whitelist.isEmpty()) it else it.filter { m -> m.metName in whitelist } } .map { DynamicTest.dynamicTest(it.metName) { - println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + analyzeAndLog(machine, pkg, it.metName) } } } @TestFactory fun jacodbTestImports(): Collection { - val packages = Files.list(Paths.get("out")) - .filter { p -> p.extension != "yaml" } - .map { Converter.unpack(Parser().deserialize(it.pathString)) } - .collect(Collectors.toList()) + val packages = Files.list(Paths.get("out")).use { paths -> + paths.filter { path -> path.extension != "yaml" } + .map { Converter.unpack(Parser().deserialize(it.pathString)) } + .collect(Collectors.toList()) + } val program = GoProgram(packages) val machine = GoMachine(program, options, customOptions) val pkg = program.findPackage("usvm/examples/imports") return methods(pkg).filter { it.metName !in longMethods }.map { DynamicTest.dynamicTest(it.metName) { - println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + analyzeAndLog(machine, pkg, it.metName) } } } @TestFactory fun jacodbTestStdStrings(): Collection { - val packages = Files.list(Paths.get("out")) - .filter { p -> p.extension != "yaml" } - .map { Converter.unpack(Parser().deserialize(it.pathString)) } - .collect(Collectors.toList()) + val packages = Files.list(Paths.get("out")).use { paths -> + paths.filter { path -> path.extension != "yaml" } + .map { Converter.unpack(Parser().deserialize(it.pathString)) } + .collect(Collectors.toList()) + } val program = GoProgram(packages) val machine = GoMachine(program, options, customOptions) val pkg = program.findPackage("flag") return methods(pkg).filter { it.metName !in longMethods }.map { DynamicTest.dynamicTest(it.metName) { - println(measureTimeMillis { println(machine.analyzeAndResolve(pkg, it.metName)) }) + analyzeAndLog(machine, pkg, it.metName) } } } + private fun analyzeAndLog(machine: GoMachine, pkg: GoPackage, methodName: String) { + val elapsedMillis = measureTimeMillis { + val result = machine.analyzeAndResolve(pkg, methodName) + logger.info { result } + } + logger.info { "Analysis took $elapsedMillis ms" } + } + private fun methods(pkg: GoPackage): List = pkg.methods.filter { !it.metName.contains('$') } private val options: UMachineOptions = UMachineOptions( @@ -81,14 +91,14 @@ class JacoDbTest { exceptionsPropagation = true, // timeout = 5_000.milliseconds, timeout = Duration.INFINITE, // for debug - stepsFromLastCovered = 1000000L, + stepsFromLastCovered = 1_000_000L, solverTimeout = Duration.INFINITE, // we do not need the timeout for a solver in tests typeOperationsTimeout = Duration.INFINITE, // we do not need the timeout for type operations in tests ) private val customOptions: GoMachineOptions = GoMachineOptions( failOnNotFullCoverage = false, - uncoveredMethods = listOf("panicRecoverComplex") + uncoveredMethods = listOf("panicRecoverComplex"), ) private val longMethods = arrayOf("loopInfinite", "loopInner", "loopCollatz", "mapLoopLen", "canVisitAllRooms") @@ -101,4 +111,4 @@ class JacoDbTest { // "shiftErrorToString" // "mapCustomDeleteSimple" ) -} \ No newline at end of file +} diff --git a/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt b/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt index a6344ca960..8e05bb0ce1 100644 --- a/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt @@ -18,4 +18,4 @@ class ModelTest { assertEquals(expected, actual) } -} \ No newline at end of file +} From 2e57e35df18fc01d2119bbaf14397ac2717238cc Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Tue, 13 Oct 2026 06:31:54 +0300 Subject: [PATCH 7/9] [Go] Bound prototype tests and close per-test machines --- .../src/test/kotlin/org/usvm/JacoDbTest.kt | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt index e1640fcac3..dd034f945a 100644 --- a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt @@ -11,17 +11,19 @@ import java.util.stream.Collectors import kotlin.io.path.extension import kotlin.io.path.pathString import kotlin.system.measureTimeMillis -import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds class JacoDbTest { @TestFactory fun jacodbTestLong(): Collection { val pkg = Converter.unpack(Parser().deserialize("out/usvm_examples.json")) val program = GoProgram(listOf(pkg)) - val machine = GoMachine(program, options, customOptions) - return methods(pkg).filter { it.metName in longMethods }.map { - DynamicTest.dynamicTest(it.metName) { - analyzeAndLog(machine, pkg, it.metName) + + return methods(pkg).filter { it.metName in longMethods }.map { method -> + DynamicTest.dynamicTest(method.metName) { + GoMachine(program, options, customOptions).use { machine -> + analyzeAndLog(machine, pkg, method.metName) + } } } } @@ -30,13 +32,15 @@ class JacoDbTest { fun jacodbTestFast(): Collection { val pkg = Converter.unpack(Parser().deserialize("out/usvm_examples.json")) val program = GoProgram(listOf(pkg)) - val machine = GoMachine(program, options, customOptions) + return methods(pkg) .filter { it.metName !in longMethods } .let { if (whitelist.isEmpty()) it else it.filter { m -> m.metName in whitelist } } - .map { - DynamicTest.dynamicTest(it.metName) { - analyzeAndLog(machine, pkg, it.metName) + .map { method -> + DynamicTest.dynamicTest(method.metName) { + GoMachine(program, options, customOptions).use { machine -> + analyzeAndLog(machine, pkg, method.metName) + } } } } @@ -49,11 +53,13 @@ class JacoDbTest { .collect(Collectors.toList()) } val program = GoProgram(packages) - val machine = GoMachine(program, options, customOptions) val pkg = program.findPackage("usvm/examples/imports") - return methods(pkg).filter { it.metName !in longMethods }.map { - DynamicTest.dynamicTest(it.metName) { - analyzeAndLog(machine, pkg, it.metName) + + return methods(pkg).filter { it.metName !in longMethods }.map { method -> + DynamicTest.dynamicTest(method.metName) { + GoMachine(program, options, customOptions).use { machine -> + analyzeAndLog(machine, pkg, method.metName) + } } } } @@ -66,11 +72,13 @@ class JacoDbTest { .collect(Collectors.toList()) } val program = GoProgram(packages) - val machine = GoMachine(program, options, customOptions) val pkg = program.findPackage("flag") - return methods(pkg).filter { it.metName !in longMethods }.map { - DynamicTest.dynamicTest(it.metName) { - analyzeAndLog(machine, pkg, it.metName) + + return methods(pkg).filter { it.metName !in longMethods }.map { method -> + DynamicTest.dynamicTest(method.metName) { + GoMachine(program, options, customOptions).use { machine -> + analyzeAndLog(machine, pkg, method.metName) + } } } } @@ -89,11 +97,10 @@ class JacoDbTest { pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), coverageZone = CoverageZone.TRANSITIVE, exceptionsPropagation = true, -// timeout = 5_000.milliseconds, - timeout = Duration.INFINITE, // for debug + timeout = 30.seconds, stepsFromLastCovered = 1_000_000L, - solverTimeout = Duration.INFINITE, // we do not need the timeout for a solver in tests - typeOperationsTimeout = Duration.INFINITE, // we do not need the timeout for type operations in tests + solverTimeout = 5.seconds, + typeOperationsTimeout = 5.seconds, ) private val customOptions: GoMachineOptions = GoMachineOptions( From 921c56da5dd02a4ac57f72fcaa9185001b726e9c Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Tue, 13 Oct 2026 06:39:29 +0300 Subject: [PATCH 8/9] [Go] Accept expression map keys from the published API --- usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index 65aac1f39c..f588036cdc 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -19,6 +19,7 @@ import org.jacodb.go.api.GoConst import org.jacodb.go.api.GoConvertExpr import org.jacodb.go.api.GoDivExpr import org.jacodb.go.api.GoEqlExpr +import org.jacodb.go.api.GoExpr import org.jacodb.go.api.GoExprVisitor import org.jacodb.go.api.GoExtractExpr import org.jacodb.go.api.GoFieldAddrExpr @@ -889,7 +890,7 @@ class GoExprVisitor( return unbox(expr.asExpr(ctx.addressSort), ctx.typeToSort(type.underlying())) } - internal fun resolveMapKey(value: GoValue): UExpr { + internal fun resolveMapKey(value: GoExpr): UExpr { val key = value.accept(this) val storageType = value.type.mapKeyStorageType() if (storageType == value.type) return key From 100079903fe65841d99c9d47fabdc1226ed70d74 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Tue, 13 Oct 2026 06:44:29 +0300 Subject: [PATCH 9/9] [Go] Verify prototype compilation and exporter contracts in CI --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f9ac76f35..be76854afa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,49 @@ jobs: path: '**/build/reports/' retention-days: 1 + ci-go: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Java JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.3' + cache-dependency-path: usvm-go/src/main/go/go.sum + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Compile Go prototype and run lint + run: >- + ./gradlew :usvm-go:compileTestKotlin :usvm-go:detektMain :usvm-go:detektTest + --configure-on-demand --no-daemon --max-workers=1 + + - name: Check Go exporter variadic method signatures + working-directory: usvm-go/src/main/go + env: + GOTOOLCHAIN: go1.22.3 + GOMAXPROCS: 1 + GOFLAGS: -p=1 + run: go test . -run '^TestMethodSignaturesPreserveVariadicParameters$' -count=1 + + - name: Upload Gradle reports + if: (!cancelled()) + uses: actions/upload-artifact@v4 + with: + name: gradle-reports-go + path: '**/build/reports/' + retention-days: 1 + ci-jvm: runs-on: ubuntu-24.04 steps: