From e961f2402db05d63669b8b3671ce0e17dc2c23f1 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Fri, 9 Oct 2026 13:01:49 +0300 Subject: [PATCH 1/6] Integrate experimental Go frontend with native regression tests Import the SSA/JacoDB frontend from buraindo/go-jacodb at 717bd41613fbc4bb8d8a0af43f3900bab824dcb8 onto current main. Add native semantic assertions and witness replay, fix the reproduced arithmetic and collection defects, and keep pointer metadata local to Go. Pin the exporter toolchain, add CI, enforce style, and document the remaining experimental boundaries. --- .github/workflows/ci.yml | 33 + build.gradle.kts | 1 + buildSrc/src/main/kotlin/Dependencies.kt | 6 + settings.gradle.kts | 1 + .../collection/PrimitiveMapCollectionApi.kt | 166 ++ .../usvm/api/collections/PrimitiveMapTest.kt | 89 ++ usvm-go/.gitignore | 3 + usvm-go/README.md | 73 + usvm-go/build.gradle.kts | 75 + 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 | 55 + usvm-go/src/main/go/model.go | 496 ++++++ usvm-go/src/main/go/pack.go | 715 +++++++++ usvm-go/src/main/go/regressions/semantics.go | 282 ++++ .../src/main/go/regressions/semantics_test.go | 119 ++ .../src/main/go/regressions/unsupported.go | 13 + usvm-go/src/main/go/ssa.go | 133 ++ usvm-go/src/main/go/util.go | 21 + .../kotlin/org/usvm/GoApplicationGraph.kt | 49 + .../src/main/kotlin/org/usvm/GoComponents.kt | 39 + usvm-go/src/main/kotlin/org/usvm/GoContext.kt | 144 ++ usvm-go/src/main/kotlin/org/usvm/GoDomain.kt | 74 + .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 1359 +++++++++++++++++ .../src/main/kotlin/org/usvm/GoInstVisitor.kt | 213 +++ usvm-go/src/main/kotlin/org/usvm/GoMachine.kt | 189 +++ .../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 | 142 ++ .../org/usvm/interpreter/GoStepAbort.kt | 4 + .../org/usvm/interpreter/GoTestInterpreter.kt | 556 +++++++ .../org/usvm/memory/GoArrayAllocation.kt | 29 + .../kotlin/org/usvm/memory/GoArrayMemory.kt | 68 + .../kotlin/org/usvm/memory/GoArrayView.kt | 43 + .../main/kotlin/org/usvm/model/Converter.kt | 750 +++++++++ .../src/main/kotlin/org/usvm/model/Model.kt | 644 ++++++++ .../src/main/kotlin/org/usvm/model/Parser.kt | 98 ++ .../org/usvm/operator/GoBinaryOperator.kt | 141 ++ .../org/usvm/operator/GoOperatorUtils.kt | 82 + .../org/usvm/operator/GoUnaryOperator.kt | 85 ++ .../kotlin/org/usvm/state/GoMethodResult.kt | 21 + .../src/main/kotlin/org/usvm/state/GoState.kt | 416 +++++ .../main/kotlin/org/usvm/state/GoStateData.kt | 58 + .../src/main/kotlin/org/usvm/type/GoType.kt | 95 ++ .../main/kotlin/org/usvm/type/GoTypeSystem.kt | 108 ++ .../main/kotlin/org/usvm/util/Extensions.kt | 30 + .../org/usvm/GoSemanticRegressionTest.kt | 115 ++ .../test/kotlin/org/usvm/GoTestFixtures.kt | 5 + .../test/kotlin/org/usvm/GoUnsupportedTest.kt | 30 + .../src/test/kotlin/org/usvm/JacoDbTest.kt | 46 + .../test/kotlin/org/usvm/model/ModelTest.kt | 22 + usvm-go/src/test/resources/logback.xml | 12 + 70 files changed, 8977 insertions(+) create mode 100644 usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt create mode 100644 usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt create mode 100644 usvm-go/.gitignore create mode 100644 usvm-go/README.md 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/regressions/semantics.go create mode 100644 usvm-go/src/main/go/regressions/semantics_test.go create mode 100644 usvm-go/src/main/go/regressions/unsupported.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/GoStepAbort.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/memory/GoArrayAllocation.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/memory/GoArrayMemory.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.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/GoSemanticRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/GoTestFixtures.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50ade7a65b..5cc6870b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,39 @@ 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: Run Go regression tests and lint + run: ./gradlew :usvm-go:check :usvm-go:detektMain :usvm-go:detektTest --configure-on-demand + + - 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: 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/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 700801642a..d5313194ab 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/api/collection/PrimitiveMapCollectionApi.kt b/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt new file mode 100644 index 0000000000..3fbee5d8ab --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt @@ -0,0 +1,166 @@ +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 < + MapType, + KeySort : USort, + ValueSort : USort, + Reg : Region, + > 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 < + MapType, + KeySort : USort, + Reg : Region, + > UState.symbolicPrimitiveMapContains( + mapRef: UHeapRef, + key: UExpr, + mapType: MapType, + keyInfo: USymbolicCollectionKeyInfo, Reg>, + ): UBoolExpr = memory.setContainsElement(mapRef, key, mapType, keyInfo) + + fun < + MapType, + KeySort : USort, + Reg : Region, + > 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 < + MapType, + USizeSort : USort, + Ctx : UContext, + 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 < + MapType, + USizeSort : USort, + Ctx : UContext, + 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) + + memory.write(mapContainsLValue, rvalue = falseExpr, guard = trueExpr) + + val updatedSize = mkSizeSubExpr(currentSize, mkSizeExpr(1)) + memory.write(UMapLengthLValue(mapRef, mapType, sizeSort), updatedSize, keyIsInMap) + } + + fun < + MapType, + USizeSort : USort, + Ctx : UContext, + KeySort : USort, + ValueSort : USort, + Reg : Region, + > UState.symbolicPrimitiveMapCopyIntoEmpty( + 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) + require(dstMapSize == mkSizeExpr(0)) { "Map copy requires an empty destination" } + + 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) + + memory.write(UMapLengthLValue(dstRef, mapType, sizeSort), srcMapSize, guard = trueExpr) + } +} diff --git a/usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt b/usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt new file mode 100644 index 0000000000..9250037919 --- /dev/null +++ b/usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt @@ -0,0 +1,89 @@ +package org.usvm.api.collections + +import io.ksmt.utils.asExpr +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.usvm.UBv32Sort +import org.usvm.api.collection.ObjectMapCollectionApi.mkSymbolicObjectMap +import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapSize +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapContains +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapCopyIntoEmpty +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapGet +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapPut +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapRemove +import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.mkSizeExpr +import org.usvm.sizeSort +import org.usvm.types.single.SingleTypeSystem +import kotlin.test.assertEquals + +class PrimitiveMapTest : SymbolicCollectionTestBase() { + private val mapType = SingleTypeSystem.SingleType + + @Test + fun overwriteAndRepeatedRemovalPreserveSize() = scope.doWithState { + val map = mkSymbolicObjectMap(mapType) + val key = ctx.mkBv(7) + val keyInfo = USizeExprKeyInfo() + + symbolicPrimitiveMapPut(map, key, ctx.mkBv(1), mapType, keyInfo) + symbolicPrimitiveMapPut(map, key, ctx.mkBv(2), mapType, keyInfo) + + assertEquals(ctx.mkSizeExpr(1), symbolicObjectMapSize(map, mapType)) + assertEquals(ctx.mkBv(2), symbolicPrimitiveMapGet(map, key, mapType, ctx.bv32Sort, keyInfo)) + + symbolicPrimitiveMapRemove(map, key, mapType, keyInfo) + symbolicPrimitiveMapRemove(map, key, mapType, keyInfo) + + assertEquals(ctx.mkSizeExpr(0), symbolicObjectMapSize(map, mapType)) + assertEquals(ctx.falseExpr, symbolicPrimitiveMapContains(map, key, mapType, keyInfo)) + } + + @Test + fun symbolicKeyEqualityControlsSize() = scope.doWithState { + val map = mkSymbolicObjectMap(mapType) + val first = ctx.mkRegisterReading(0, ctx.bv32Sort) + val second = ctx.mkRegisterReading(1, ctx.bv32Sort) + val keyInfo = USizeExprKeyInfo() + + symbolicPrimitiveMapPut(map, first, ctx.mkBv(1), mapType, keyInfo) + symbolicPrimitiveMapPut(map, second, ctx.mkBv(2), mapType, keyInfo) + val size = symbolicObjectMapSize(map, mapType).asExpr(ctx.sizeSort) + + checkWithSolver { + assertImpossible { + mkAnd(mkEq(first, second), mkNot(mkEq(size, mkBv(1)))) + } + assertImpossible { + mkAnd(mkNot(mkEq(first, second)), mkNot(mkEq(size, mkBv(2)))) + } + } + } + + @Test + fun copyPreservesEntriesAndSize() = scope.doWithState { + val source = mkSymbolicObjectMap(mapType) + val destination = mkSymbolicObjectMap(mapType) + val key = ctx.mkBv(7) + val keyInfo = USizeExprKeyInfo() + symbolicPrimitiveMapPut(source, key, ctx.mkBv(42), mapType, keyInfo) + + symbolicPrimitiveMapCopyIntoEmpty(destination, source, mapType, ctx.bv32Sort, ctx.bv32Sort, keyInfo) + + assertEquals(ctx.mkSizeExpr(1), symbolicObjectMapSize(destination, mapType)) + assertEquals(ctx.trueExpr, symbolicPrimitiveMapContains(destination, key, mapType, keyInfo)) + assertEquals(ctx.mkBv(42), symbolicPrimitiveMapGet(destination, key, mapType, ctx.bv32Sort, keyInfo)) + } + + @Test + fun copyingIntoNonemptyMapIsRejected() = scope.doWithState { + val source = mkSymbolicObjectMap(mapType) + val destination = mkSymbolicObjectMap(mapType) + val keyInfo = USizeExprKeyInfo() + symbolicPrimitiveMapPut(destination, ctx.mkBv(1), ctx.mkBv(42), mapType, keyInfo) + + assertThrows { + symbolicPrimitiveMapCopyIntoEmpty(destination, source, mapType, ctx.bv32Sort, ctx.bv32Sort, keyInfo) + } + } +} 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/README.md b/usvm-go/README.md new file mode 100644 index 0000000000..343be952ba --- /dev/null +++ b/usvm-go/README.md @@ -0,0 +1,73 @@ +# Experimental Go frontend + +This module imports the SSA/JacoDB Go prototype from `buraindo/go-jacodb` +(commit `717bd41613fbc4bb8d8a0af43f3900bab824dcb8`) and adapts it to the current USVM core. +It is an experimental frontend, with the validation and limitations below. + +## Build and tests + +Use a Go installation that supports `GOTOOLCHAIN` and Java 11 or later. Gradle pins +Go to **1.22.3**, matching `src/main/go/go.mod`. The pinned `x/tools v0.24.0` exporter +is incompatible with Go 1.26. The Go-specific JacoDB API is pinned separately to +`816194b963`; it does not change other frontends' JacoDB dependencies. + +From the repository root: + +```sh +./gradlew :usvm-go:check :usvm-go:detektMain :usvm-go:detektTest --configure-on-demand +``` + +The test task generates SSA JSON and the native oracle under `usvm-go/build/generated/go`. +No checked-in dumps or pre-existing `out` directory are required. Tests have finite +machine/solver timeouts, close machines, and are bounded by a 15-minute Gradle timeout. + +`GoSemanticRegressionTest` compares zero-argument scalar results and panic outcomes +with a native Go execution. It also asks USVM for branch witnesses, then executes +those concrete inputs in Go, including a write through an aliased slice after a fork. +The scalar cases cover shifts, bit operations, native integer width and unsigned +widening, slice length/capacity, aliasing, copy/append, array pointers, map size, +string byte content/order/conversion, and selected panic paths. + +`GoUnsupportedTest` checks that goroutines and comparison of two strings with +symbolic lengths produce explicit unsupported results. `JacoDbTest` retains the +98 fast prototype examples as execution/coverage smoke checks; these do not assert +native output values. `panicRecoverComplex` remains an explicit partial-coverage +exception. `ModelTest` checks SSA JSON round-tripping. + +The five slow prototype examples are manual and bounded: + +```sh +./gradlew :usvm-go:manualTest --configure-on-demand +``` + +`generateGoImports` can export the import examples for investigation. The original +import/standard-library exploratory factories depended on manually prepared dumps; +they are not included in the default suite. + +## Representation and boundaries + +The exporter records the target `int` width and raw string bytes. Arrays and slices +share storage by element type. Slice headers record backing storage, offset, length +and capacity; `copy` and `append` operate on these views. Go pointers to fields or +array elements are frontend-local metadata, without changing core expression +transformers. State merging is rejected when frontend metadata differs. + +Passing these tests does **not** establish complete Go semantics or arbitrary-project +support. In particular: + +- Channels, goroutines, `select`, generic multi-conversions and unsafe pointer + conversions are unsupported. Unsupported callees are reported explicitly. +- String comparison requires at least one concrete length. Rune conversion and + complete UTF-8 string iteration are not implemented. Resolved strings are decoded + as UTF-8, so arbitrary invalid byte sequences are not preserved in result text. +- Collection sizes use a nonnegative BV32 domain; symbolic sizes are restricted to + that domain. Input slices currently model capacity as length, and materialized + array/slice/string models are capped at 10,000 elements. +- Unknown external calls and function parameters retain the prototype's mocking + behavior. `GoFunctionReference` identifies a function input that cannot be replayed. +- General array/struct value-copy behavior, pointer/interface equality, reference + map keys and nil-map operations need further semantic validation. The native + regression suite does not claim coverage of these areas. +- Package loading, standard-library integration and arbitrary repository workflows + have not been validated end to end. Instruction coverage alone is not a semantic + correctness check. diff --git a/usvm-go/build.gradle.kts b/usvm-go/build.gradle.kts new file mode 100644 index 0000000000..5bff557089 --- /dev/null +++ b/usvm-go/build.gradle.kts @@ -0,0 +1,75 @@ +import io.gitlab.arturbosch.detekt.Detekt +import java.time.Duration + +plugins { + kotlin("jvm") + id("usvm.kotlin-conventions") + kotlin("plugin.serialization") version Versions.kotlin +} + +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) + testImplementation(Libs.logback) +} + +val generatedGo = layout.buildDirectory.dir("generated/go") +val goSources = fileTree("src/main/go") { include("**/*.go", "go.mod", "go.sum") } + +fun registerGoExport(taskName: String, packageName: String, directory: String) = tasks.register(taskName) { + group = "verification" + description = "Exports $packageName as SSA JSON." + workingDir(layout.projectDirectory.dir("src/main/go")) + environment("GOTOOLCHAIN", "go1.22.3") + inputs.files(goSources) + outputs.dir(generatedGo.map { it.dir(directory) }) + commandLine("go", "run", ".", "-packageName", packageName, "-dump-ssa=false", + "-output-dir", generatedGo.get().dir(directory).asFile.absolutePath) +} + +val generateGoIr = registerGoExport(taskName = "generateGoIr", packageName = "usvm/examples", directory = "examples") +val generateGoRegressions = registerGoExport(taskName = "generateGoRegressions", packageName = "usvm/regressions", directory = "regressions") +val generateGoImports = registerGoExport(taskName = "generateGoImports", packageName = "usvm/examples/imports", directory = "imports") + +val generateGoOracle by tasks.registering(Exec::class) { + group = "verification" + description = "Executes the regression samples with native Go." + workingDir(layout.projectDirectory.dir("src/main/go")) + environment("GOTOOLCHAIN", "go1.22.3") + environment("USVM_GO_ORACLE_FILE", generatedGo.get().file("native-oracle.json").asFile.absolutePath) + inputs.files(goSources) + outputs.file(generatedGo.map { it.file("native-oracle.json") }) + doFirst { generatedGo.get().asFile.mkdirs() } + commandLine("go", "test", "./regressions", "-run", "TestNativeOracle", "-count=1") +} + +val compileGoReplay by tasks.registering(Exec::class) { + group = "verification" + description = "Compiles the native witness replay runner." + workingDir(layout.projectDirectory.dir("src/main/go")) + environment("GOTOOLCHAIN", "go1.22.3") + inputs.files(goSources) + outputs.file(generatedGo.map { it.file("native-replay.test") }) + commandLine("go", "test", "-c", "-o", generatedGo.get().file("native-replay.test").asFile.absolutePath, "./regressions") +} + +tasks.withType().configureEach { + systemProperty("usvm.go.generatedDir", generatedGo.get().asFile.absolutePath) + timeout.set(Duration.ofMinutes(15)) +} + +tasks.test { + dependsOn(generateGoIr, generateGoRegressions, generateGoOracle, compileGoReplay) +} + +tasks.named("manualTest") { + dependsOn(generateGoIr) +} + +tasks.withType().configureEach { + ignoreFailures = false +} 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..29f402d145 --- /dev/null +++ b/usvm-go/src/main/go/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "flag" + "log" + "os" + "runtime/pprof" + "time" +) + +var ( + outputDir = flag.String("output-dir", "out", "Directory for serialized SSA packages") + 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(*outputDir) + } + + 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..dcd0298448 --- /dev/null +++ b/usvm-go/src/main/go/model.go @@ -0,0 +1,496 @@ +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"` + IntSize int `yaml:"int_size" json:"int_size"` + + 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 { + Bytes *string `yaml:"bytes,omitempty" json:"bytes,omitempty"` + 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..d5280d890c --- /dev/null +++ b/usvm-go/src/main/go/pack.go @@ -0,0 +1,715 @@ +package main + +import ( + "encoding/base64" + "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: packConstant(member.Value.Type().String(), member.Value.Value), + } + 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(nil, types.Typ[types.UntypedNil]) + mx ssa.Value = ssa.NewConst(nil, types.Typ[types.UntypedNil]) + ) + 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 + return Const{ + CommonValue: common, + Value: packConstant(value.Type().String(), value.Value), + } + 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 +} + +func packConstant(typeName string, value constant.Value) NamedConstValue { + result := NamedConstValue{Type: typeName, Value: "nil"} + if value == nil { + return result + } + result.Value = value.String() + if value.Kind() == constant.String { + encoded := base64.StdEncoding.EncodeToString([]byte(constant.StringVal(value))) + result.Bytes = &encoded + } + return result +} diff --git a/usvm-go/src/main/go/regressions/semantics.go b/usvm-go/src/main/go/regressions/semantics.go new file mode 100644 index 0000000000..33b5f5f8b2 --- /dev/null +++ b/usvm-go/src/main/go/regressions/semantics.go @@ -0,0 +1,282 @@ +package regressions + +func shiftByBitWidth() uint32 { + value := uint32(1) + count := uint32(32) + return value << count +} + +func bitwiseComplement() int32 { + value := int32(1) + return ^value +} + +func bitwiseAndNot() uint32 { + left := uint32(7) + right := uint32(3) + return left &^ right +} + +func nativeIntWidth() int { + value := int(2147483647) + return value + 1 +} + +func sliceCapacity() int { + values := make([]int, 1, 3) + return cap(values) +} + +func mapHintLength() int { + values := make(map[int]int, 8) + return len(values) +} + +func mapInsertLength() int { + values := make(map[int]int) + values[1] = 2 + return len(values) +} + +func sliceAlias() int { + values := []int{1} + alias := values[:] + alias[0] = 2 + return values[0] +} + +func stringEquality() bool { + left := "same" + right := "same" + return left == right +} + +func stringInequality() bool { + left, right := "abcd", "abce" + return left != right +} + +func stringLengthDifference() bool { + left, right := "a", "ab" + return left == right +} + +func stringOrdering() bool { + left, right := "\xff", "z" + return left > right +} + +func stringPrefixOrdering() bool { + left, right := "a", "ab" + return left < right +} + +func emptyStringEquality() bool { + left, right := "", "" + return left == right +} + +func stringConcatenation() bool { + left, right := "ab", "cd" + combined := left + right + expected := "abcd" + return combined == expected +} + +func stringBytesCopy() int { + original := "abc" + values := []byte(original) + values[0] = 'x' + return int(original[0]) +} + +func bytesStringCopy() int { + values := []byte{'a', 'b'} + original := string(values) + values[0] = 'x' + return int(original[0]) +} + +func sliceOffsetAlias() int { + values := []int{1, 2, 3} + alias := values[1:] + alias[0] = 7 + return values[1] +} + +func sliceFullCapacity() int { + values := make([]int, 2, 5) + return cap(values[1:2:3]) +} + +func sliceCopyOffset() int { + values := []int{1, 2, 3, 4} + copy(values[1:3], values[2:4]) + return values[1]*10 + values[2] +} + +func sliceCopyString() int { + values := make([]byte, 3) + source := "abc" + copy(values[1:], source) + return int(values[1])*1000 + int(values[2]) +} + +func sliceAppendReuse() int { + values := make([]int, 1, 3) + alias := values[:3] + result := append(values, 7) + return alias[1]*10 + result[1] +} + +func sliceAppendAllocate() int { + values := []int{1, 2} + result := append(values, 7) + result[0] = 9 + return values[0]*10 + result[2] +} + +func sliceAppendOffset() int { + values := []int{1, 2, 3, 4} + result := append(values[1:2], 7) + return values[2]*10 + result[1] +} + +func sliceArrayPointerAlias() int { + values := []int{1, 2, 3} + pointer := (*[2]int)(values[1:]) + pointer[0] = 7 + return values[1] +} + +func nilSliceLength() int { + var values []int + return len(values) + cap(values) +} + +func nilSliceAppend() int { + var values []int + result := append(values, 7) + return result[0] +} + +func mapOverwriteLength() int { + values := make(map[int]int) + values[1] = 2 + values[1] = 3 + return len(values) +} + +func mapDeleteLength() int { + values := make(map[int]int) + values[1] = 2 + delete(values, 1) + delete(values, 1) + return len(values) +} + +func bitwiseAndOr() uint32 { + left, right := uint32(7), uint32(3) + return (left & right) | uint32(8) +} + +func oversizedShiftCount() uint32 { + value, count := uint32(1), uint64(1)<<32 + return value << count +} + +func signedRightShift() int32 { + value, count := int32(-7), uint64(32) + return value >> count +} + +func unsignedRightShift() uint32 { + value, count := uint32(0xffffffff), uint64(32) + return value >> count +} + +func nativeIntOverflow() bool { + maximum := int(^uint(0) >> 1) + return maximum+1 < 0 +} + +func unsignedWidening() uint64 { + value := uint32(0xffffffff) + return uint64(value) +} + +func symbolicBranch(value int) int { + if value < 0 { + return -1 + } + if value == 0 { + return 0 + } + return 1 +} + +func oversizedIndex() int { + values := []int{7} + index := int64(1) << 32 + return values[index] +} + +func negativeIndex() int { + values := []int{7} + index := -1 + return values[index] +} + +func negativeShift() uint32 { + value := uint32(1) + count := -1 + return value << count +} + +func divideByZero() int { + value, divisor := 7, 0 + return value / divisor +} + +func remainderByZero() int { + value, divisor := 7, 0 + return value % divisor +} + +func utf8StringLength() int { + value := "\x00я" + return len(value) +} + +func unsignedSliceLength() int { + length := uint8(128) + return len(make([]byte, length)) +} + +func narrowIndex() int { + values := [300]int{50: 7} + index := uint8(50) + return values[index] +} + +func negativeSliceHigh() int { + values := []int{7} + high := -1 + return len(values[:high]) +} + +func symbolicSliceAlias(value int) int { + values := []int{1, 2} + alias := values[1:] + if value < 0 { + alias[0] = 3 + } else { + alias[0] = 7 + } + return values[1] +} + +func unsignedResultWidth() uint64 { + value := uint64(0xffffffffffffffff) + return value +} diff --git a/usvm-go/src/main/go/regressions/semantics_test.go b/usvm-go/src/main/go/regressions/semantics_test.go new file mode 100644 index 0000000000..e10b9d4cec --- /dev/null +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -0,0 +1,119 @@ +package regressions + +import ( + "encoding/json" + "fmt" + "os" + "testing" +) + +func TestNativeOracle(t *testing.T) { + cases := map[string]func() any{ + "unsignedResultWidth": func() any { return unsignedResultWidth() }, + "shiftByBitWidth": func() any { return shiftByBitWidth() }, + "bitwiseComplement": func() any { return bitwiseComplement() }, + "bitwiseAndNot": func() any { return bitwiseAndNot() }, + "nativeIntWidth": func() any { return nativeIntWidth() }, + "sliceCapacity": func() any { return sliceCapacity() }, + "mapHintLength": func() any { return mapHintLength() }, + "mapInsertLength": func() any { return mapInsertLength() }, + "sliceAlias": func() any { return sliceAlias() }, + "stringEquality": func() any { return stringEquality() }, + "stringInequality": func() any { return stringInequality() }, + "stringLengthDifference": func() any { return stringLengthDifference() }, + "stringOrdering": func() any { return stringOrdering() }, + "stringPrefixOrdering": func() any { return stringPrefixOrdering() }, + "emptyStringEquality": func() any { return emptyStringEquality() }, + "stringConcatenation": func() any { return stringConcatenation() }, + "stringBytesCopy": func() any { return stringBytesCopy() }, + "bytesStringCopy": func() any { return bytesStringCopy() }, + "sliceOffsetAlias": func() any { return sliceOffsetAlias() }, + "sliceFullCapacity": func() any { return sliceFullCapacity() }, + "sliceCopyOffset": func() any { return sliceCopyOffset() }, + "sliceCopyString": func() any { return sliceCopyString() }, + "sliceAppendReuse": func() any { return sliceAppendReuse() }, + "sliceAppendAllocate": func() any { return sliceAppendAllocate() }, + "sliceAppendOffset": func() any { return sliceAppendOffset() }, + "sliceArrayPointerAlias": func() any { return sliceArrayPointerAlias() }, + "nilSliceLength": func() any { return nilSliceLength() }, + "nilSliceAppend": func() any { return nilSliceAppend() }, + "mapOverwriteLength": func() any { return mapOverwriteLength() }, + "mapDeleteLength": func() any { return mapDeleteLength() }, + "bitwiseAndOr": func() any { return bitwiseAndOr() }, + "oversizedShiftCount": func() any { return oversizedShiftCount() }, + "signedRightShift": func() any { return signedRightShift() }, + "unsignedRightShift": func() any { return unsignedRightShift() }, + "nativeIntOverflow": func() any { return nativeIntOverflow() }, + "unsignedWidening": func() any { return unsignedWidening() }, + "oversizedIndex": func() any { return oversizedIndex() }, + "negativeIndex": func() any { return negativeIndex() }, + "negativeShift": func() any { return negativeShift() }, + "divideByZero": func() any { return divideByZero() }, + "remainderByZero": func() any { return remainderByZero() }, + "utf8StringLength": func() any { return utf8StringLength() }, + "unsignedSliceLength": func() any { return unsignedSliceLength() }, + "narrowIndex": func() any { return narrowIndex() }, + "negativeSliceHigh": func() any { return negativeSliceHigh() }, + } + results := make(map[string]string, len(cases)) + + for name, run := range cases { + results[name] = captureNativeResult(run) + } + + output := os.Getenv("USVM_GO_ORACLE_FILE") + if output == "" { + t.Logf("Native Go results: %v", results) + return + } + + data, err := json.Marshal(results) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(output, data, 0o600); err != nil { + t.Fatal(err) + } +} + +func TestReplaySymbolicInputs(t *testing.T) { + filename := os.Getenv("USVM_GO_REPLAY_FILE") + if filename == "" { + t.Skip("No symbolic inputs supplied") + } + data, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + var inputs []int + if err := json.Unmarshal(data, &inputs); err != nil { + t.Fatal(err) + } + outputs := make([]int, len(inputs)) + for index, input := range inputs { + switch os.Getenv("USVM_GO_REPLAY_METHOD") { + case "symbolicBranch": + outputs[index] = symbolicBranch(input) + case "symbolicSliceAlias": + outputs[index] = symbolicSliceAlias(input) + default: + t.Fatal("Unknown replay method") + } + } + data, err = json.Marshal(outputs) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filename, data, 0o600); err != nil { + t.Fatal(err) + } +} + +func captureNativeResult(run func() any) (result string) { + defer func() { + if recover() != nil { + result = "panic" + } + }() + return fmt.Sprint(run()) +} diff --git a/usvm-go/src/main/go/regressions/unsupported.go b/usvm-go/src/main/go/regressions/unsupported.go new file mode 100644 index 0000000000..8a9e8e02b7 --- /dev/null +++ b/usvm-go/src/main/go/regressions/unsupported.go @@ -0,0 +1,13 @@ +package regressions + +func unsupportedGoroutine(value int) { + go symbolicBranch(value) +} + +func unsupportedCaller(value int) { + unsupportedGoroutine(value) +} + +func symbolicStringEquality(left, right string) bool { + return left == right +} diff --git a/usvm-go/src/main/go/ssa.go b/usvm-go/src/main/go/ssa.go new file mode 100644 index 0000000000..0eff930aef --- /dev/null +++ b/usvm-go/src/main/go/ssa.go @@ -0,0 +1,133 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "go/token" + "go/types" + "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 + intSize int +} + +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(), + intSize: int(initialPackages[0].TypesSizes.Sizeof(types.Typ[types.Int]) * 8), + } + + 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(outputDir string) { + for _, pkg := range s.packages { + now := time.Now() + s.writePackage(pkg, outputDir) + log.Printf("%s serialized! Took %.3f seconds\n", pkg.String(), time.Since(now).Seconds()) + } +} + +func (s *SSA) writePackage(p *ssa.Package, outputDir string) { + 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) + pkg.IntSize = s.intSize + 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..c8a04233e7 --- /dev/null +++ b/usvm-go/src/main/go/util.go @@ -0,0 +1,21 @@ +package main + +import ( + "io" + "log" +) + +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..1f5fb1d0a8 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt @@ -0,0 +1,49 @@ +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 listOfNotNull(callExpr.callee).asSequence() + } + + override fun callers(method: GoMethod): Sequence { + return method.blocks.asSequence().flatMap { it.instructions } + } + + 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() + } +} 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..3c80f3df0d --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt @@ -0,0 +1,39 @@ +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) + } +} 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..845563a538 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt @@ -0,0 +1,144 @@ +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, + val intSize: Int, +) : 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.getValue(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.getValue(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 -> { + error("Inconsistent Go interpreter state") + } + } + + private fun setMethodInfo(method: GoMethod, info: GoMethodInfo) { + methodInfo[method] = info + } + + private fun getArgsCount(method: GoMethod): Int = methodInfo.getValue(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.UINTPTR -> { + if (intSize == Int.SIZE_BITS) bv32Sort else bv64Sort + } + GoBasicTypes.INT32, GoBasicTypes.UINT32, GoBasicTypes.RUNE -> { + bv32Sort + } + GoBasicTypes.INT8, GoBasicTypes.UINT8 -> { + bv8Sort + } + GoBasicTypes.INT16, GoBasicTypes.UINT16 -> { + bv16Sort + } + GoBasicTypes.INT64, GoBasicTypes.UINT64, GoBasicTypes.UNSAFE_POINTER -> { + bv64Sort + } + GoBasicTypes.FLOAT32 -> { + fp32Sort + } + GoBasicTypes.FLOAT64 -> { + fp64Sort + } + else -> { + addressSort + } + } +} 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..71660a5932 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt @@ -0,0 +1,74 @@ +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, + val intSize: Int, +) { + private val methodsIndex: Map = methods.associateBy { it.metName } + + fun findMethod(name: String): GoMethod { + return methodsIndex[name] ?: throw UnknownMethodException(name) + } +} + +class GoProgram(packages: List) { + val intSize: Int = packages.map { it.intSize }.distinct().single().also { + require(it == Int.SIZE_BITS || it == Long.SIZE_BITS) { "Unsupported Go int width: $it" } + } + + 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 separator = name.lastIndexOf('.') + val packageName = name.substring(0, separator) + val methodName = name.substring(separator + 1) + return findPackage(packageName).findMethod(methodName) + } + + return findPackage(location.method.packageName).findMethod(name) + } + + fun findInitMethods(packageName: String): List { + return packagesIndex.getValue(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..4318ae18eb --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -0,0 +1,1359 @@ +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.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.symbolicPrimitiveMapCopyIntoEmpty +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapGet +import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapRemove +import org.usvm.api.readField +import org.usvm.api.refSetContainsElement +import org.usvm.api.setContainsElement +import org.usvm.api.typeStreamOf +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.GoStepAbort +import org.usvm.interpreter.GoStepScope +import org.usvm.memory.GoArrayView +import org.usvm.memory.ULValue +import org.usvm.memory.URegisterStackLValue +import org.usvm.memory.allocateGoArray +import org.usvm.memory.arrayStorageType +import org.usvm.memory.arrayView +import org.usvm.memory.copyGoArray +import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.memory.readGoArrayIndex +import org.usvm.memory.readGoArrayLength +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.hasUnsupportedInstructions +import org.usvm.util.isInit +import java.util.Base64 + +@Suppress("LargeClass") // One visitor implements the SSA expression interface. +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 GoStepAbort() + memory.typeStreamOf(instance).first() + } + program.findMethod(expr.location, "(${type.typeName}).${checkNotNull(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.hasUnsupportedInstructions()) { + return unsupportedExpr("method ${method.metName} contains unsupported instructions") + } + 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 = checkNotNull(node.parent) { "Phi has no predecessor in the execution path" } + } + 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) + val sourceType = expr.operand.type.underlying() + val targetType = expr.type.underlying() + if (sourceType == GoBasicTypes.STRING || targetType == GoBasicTypes.STRING) { + val sliceType = (if (sourceType == GoBasicTypes.STRING) targetType else sourceType) as? SliceType + if (sliceType?.elementType != GoBasicTypes.UINT8) { + return unsupportedExpr("string conversions other than []byte") + } + val source = unboxNamedRef(value.asExpr(ctx.addressSort), expr.operand.type) + return scope.calcOnState { + val view = arrayView(source, sourceType) + val destination = memory.allocateGoArray(targetType, ctx.sizeSort, view.length) + memory.copyGoArray( + view.backing, + destination, + view.storageType, + ctx.bv8Sort, + view.offset, + ctx.mkSizeExpr(0), + view.length + ) + tryBox(destination, expr.type) + } + } + val unsafeConversion = sourceType == GoBasicTypes.UNSAFE_POINTER || targetType == GoBasicTypes.UNSAFE_POINTER + if (unsafeConversion || targetType is PointerType) { + return unsupportedExpr("unsafe pointer conversion") + } + val operand = unboxNamedPrimitive(value, expr.operand.type) + val targetSort = ctx.typeToSort(targetType) + val converted = if (operand.sort is UBvSort && targetSort is UBvSort) { + val signed = !(sourceType as BasicType).typeName.startsWith("uint") + bv(operand).mkNarrow(targetSort.sizeBits.toInt(), signed = signed) + } else { + ctx.mkPrimitiveCast(operand, targetSort) + } + return tryBox(converted, expr.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 view = scope.calcOnState { arrayView(slice, sliceType) } + val arrayType = (expr.type as PointerType).baseType.underlying() as ArrayType + val arrayLength = ctx.mkSizeExpr(arrayType.len.toInt()) + checkSliceToArrayPointerLength(view.length, arrayLength) ?: throw GoStepAbort() + + return scope.calcOnState { + val array = memory.allocateGoArray(arrayType, ctx.sizeSort, arrayLength) + data.arrayViews[array] = view.copy(length = arrayLength, capacity = arrayLength) + 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 GoStepAbort() + } + + return value + } + + override fun visitGoMakeClosureExpr(expr: GoMakeClosureExpr): UExpr { + return ctx.mkConst(expr.func.name, ctx.addressSort) + } + + override fun visitGoMakeMapExpr(expr: GoMakeMapExpr): UExpr { + val mapType = expr.type.underlying() + + + return scope.calcOnState { + val ref = memory.allocConcrete(mapType) + memory.write(UMapLengthLValue(ref, mapType, ctx.sizeSort), ctx.mkSizeExpr(0), 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 length = collectionSize(expr.len) + val capacity = collectionSize(expr.cap) + + checkLength(length) ?: throw GoStepAbort() + checkLength(capacity) ?: throw GoStepAbort() + checkIndexOutOfBounds(length, ctx.mkSizeAddExpr(capacity, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() + + return scope.calcOnState { + val type = expr.type.underlying() + val backing = memory.allocateGoArray(type, ctx.sizeSort, capacity) + val slice = memory.allocateGoArray(type, ctx.sizeSort, length) + data.arrayViews[slice] = GoArrayView( + backing, type.arrayStorageType(), offset = ctx.mkSizeExpr(0), length = length, capacity = capacity, + ) + tryBox(slice, 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 view = scope.calcOnState { arrayView(array, arrayType) } + val low = collectionSize(expr.low) + val high = sliceBound(expr.high, view.length) + val limit = sliceBound(expr.max, view.capacity) + val count = ctx.mkSizeSubExpr(high, low) + + checkLength(view.length) ?: throw GoStepAbort() + checkNegativeIndex(low) ?: throw GoStepAbort() + checkNegativeIndex(high) ?: throw GoStepAbort() + checkNegativeIndex(limit) ?: throw GoStepAbort() + checkIndexOutOfBounds(low, ctx.mkSizeAddExpr(high, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() + checkIndexOutOfBounds(high, ctx.mkSizeAddExpr(limit, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() + checkIndexOutOfBounds(limit, ctx.mkSizeAddExpr(view.capacity, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() + + val result = scope.calcOnState { + val reference = memory.allocateGoArray(expr.type.underlying(), ctx.sizeSort, count) + data.arrayViews[reference] = view.copy( + offset = ctx.mkSizeAddExpr(view.offset, low), + length = count, + capacity = ctx.mkSizeSubExpr(limit, low), + ) + reference + } + val slice = ctx.mkIte(ctx.mkHeapRefEq(array, ctx.nullRef), ctx.nullRef, result) + return tryBox(slice, expr.type) + } + + private fun sliceBound(value: GoValue, default: UExpr): UExpr { + return if (value is GoNullConstant) default else collectionSize(value) + } + + private fun collectionSize(value: GoValue): UExpr { + val original = bv(unboxNamedPrimitive(value.accept(this), value.type)) + val unsigned = (value.type.underlying() as BasicType).typeName.startsWith("uint") + if (!unsigned) { + scope.fork(ctx.mkBvSignedGreaterOrEqualExpr(original, ctx.mkBv(0, original.sort)), blockOnFalseState = { + panic("negative length or slice bound") + }) ?: throw GoStepAbort() + } + // Collection memory uses BV32 sizes; restrict symbolic sizes to its representable domain. + if (original.sort.sizeBits > Int.SIZE_BITS.toUInt()) { + scope.assert(ctx.mkBvUnsignedLessOrEqualExpr(original, ctx.mkBv(Int.MAX_VALUE.toLong(), original.sort))) + ?: throw GoStepAbort() + } + return original.mkNarrow(Int.SIZE_BITS, signed = false).asExpr(ctx.sizeSort) + } + + 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 GoStepAbort() + + 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 GoStepAbort() + return scope.calcOnState { + memory.readField(struct, expr.field, ctx.typeToSort(expr.type)) + } + } + + override fun visitGoIndexAddrExpr(expr: GoIndexAddrExpr): UExpr { + val (view, index) = visitIndexExpr(expr.instance, expr.index) + + val elementType = (expr.type as PointerType).baseType + val elementLValue = UArrayIndexLValue(ctx.typeToSort(elementType), view.backing, index, view.storageType) + return mkPointer(elementType, elementLValue) + } + + override fun visitGoIndexExpr(expr: GoIndexExpr): UExpr { + val (view, index) = visitIndexExpr(expr.instance, expr.index) + return scope.calcOnState { + memory.readGoArrayIndex(view.backing, index, view.storageType, 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 GoStepAbort() + scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw GoStepAbort() + + 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 -> { + error("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.readGoArrayIndex(collection, index, collectionType, ctx.bv8Sort) + val length = memory.readGoArrayLength(collection, collectionType, ctx.sizeSort) + val ok = ctx.mkAnd(notNull, ctx.mkBvSignedLessExpr(index, length)) + + checkLength(length) ?: throw GoStepAbort() + + 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 GoStepAbort() + + 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 -> { + error("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 GoStepAbort() + 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.toLong(), typeToSort(value.type.underlying()) as UBvSort), 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.toLong(), typeToSort(value.type.underlying()) as UBvSort), 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.toLong(), typeToSort(value.type.underlying()) as UBvSort), 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(Base64.getDecoder().decode(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) + } + + @Suppress("ThrowsCount") // Failed scope checks stop this hop. + private fun visitGoBinaryExpr(expr: GoBinaryExpr): UExpr { + if (expr.lhv.type.underlying() == GoBasicTypes.STRING && expr.rhv.type.underlying() == GoBasicTypes.STRING) { + return if (expr is GoAddExpr) { + tryBox(appendArray(expr.lhv, expr.rhv), expr.type) + } else { + compareStrings(expr) + } + } + + val lhv = expr.lhv.accept(this) + val rhv = expr.rhv.accept(this) + + val isEquality = expr is GoEqlExpr || expr is GoNeqExpr + val hasNilOperand = expr.lhv is GoNullConstant || expr.rhv is GoNullConstant + val bothReferences = lhv.sort == ctx.addressSort && rhv.sort == ctx.addressSort + if (isEquality && hasNilOperand && bothReferences) { + val equal = ctx.mkHeapRefEq(lhv.asExpr(ctx.addressSort), rhv.asExpr(ctx.addressSort)) + return if (expr is GoEqlExpr) equal else ctx.mkNot(equal) + } + + val operandType = expr.lhv.type.underlying() + val signed = operandType is BasicType && !operandType.typeName.startsWith("ui") + val lhs = unboxNamedPrimitive(lhv, expr.lhv.type) + val rhs = unboxNamedPrimitive(rhv, expr.rhv.type) + if ((expr is GoDivExpr || expr is GoModExpr) && rhs.sort is UBvSort) { + scope.fork(ctx.mkNot(ctx.mkEq(bv(rhs), ctx.mkBv(0, bv(rhs).sort))), blockOnFalseState = { + panic("integer divide by zero") + }) ?: throw GoStepAbort() + } + val isShift = expr is GoShrExpr || expr is GoShlExpr + val signedShift = isShift && !(expr.rhv.type.underlying() as BasicType).typeName.startsWith("uint") + if (signedShift) { + val count = bv(rhs) + scope.fork(ctx.mkBvSignedGreaterOrEqualExpr(count, ctx.mkBv(0, count.sort)), blockOnFalseState = { + panic("negative shift amount") + }) ?: throw GoStepAbort() + } + + 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)) + + 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 -> { + GoUnaryOperator.Complement(x) + } + 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 originalIndex = bv(unboxNamedPrimitive(idx.accept(this), idx.type)) + val view = scope.calcOnState { arrayView(array, arrayType) } + val width = maxOf(originalIndex.sort.sizeBits, view.length.sort.sizeBits).toInt() + val fullIndex = originalIndex.mkNarrow(width, signed = false) + val fullLength = bv(view.length).mkNarrow(width, signed = false) + scope.fork(ctx.mkBvUnsignedLessExpr(fullIndex, fullLength), blockOnFalseState = { + panic("index out of bounds") + }) ?: throw GoStepAbort() + val index = toSizeExpr(originalIndex) + + return view to ctx.mkSizeAddExpr(view.offset, index) + } + + private fun normalize(lhs: UExpr, rhs: UExpr, goExpr: GoBinaryExpr): UExpr = with( + ctx + ) { + if (goExpr !is GoShrExpr && goExpr !is GoShlExpr) return rhs + + val valueSort = bv(lhs).sort + val count = bv(rhs) + val width = mkBv(valueSort.sizeBits.toLong(), count.sort) + val tooLarge = mkBvUnsignedGreaterOrEqualExpr(count, width) + val normalized = mkPrimitiveCast(count, valueSort).asExpr(valueSort) + mkIte(tooLarge, mkBv(valueSort.sizeBits.toLong(), valueSort), normalized) + } + + private fun toSizeExpr(expr: UExpr): UExpr = + ctx.mkPrimitiveCast(expr, ctx.sizeSort).asExpr(ctx.sizeSort) + + 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 GoStepAbort() + checkNotNull(pointer) ?: throw GoStepAbort() + 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 GoStepAbort() + checkNotNull(expr) ?: throw GoStepAbort() + 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") + }) + + @Suppress("ThrowsCount") // Failed scope checks stop this hop. + private fun callBuiltin(method: GoBuiltin, args: List, returnType: GoType): UExpr { + return when (method.name) { + "append" -> { + tryBox(appendArray(args[0], args[1]), returnType) + } + "copy" -> { + val source = unboxNamedRef(args[1].accept(this).asExpr(ctx.addressSort), args[1].type) + val destination = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) + val sliceType = args[0].type.underlying() as SliceType + val count = scope.calcOnState { + val sourceView = arrayView(source, args[1].type) + val destinationView = arrayView(destination, args[0].type) + val copied = ctx.mkIte( + ctx.mkSizeLtExpr(sourceView.length, destinationView.length), + sourceView.length, + destinationView.length + ) + memory.copyGoArray( + sourceView.backing, + destinationView.backing, + sourceView.storageType, + ctx.typeToSort(sliceType.elementType), + sourceView.offset, + destinationView.offset, + copied + ) + copied + } + ctx.mkPrimitiveCast(count, ctx.typeToSort(returnType)) + } + + "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 GoStepAbort() + 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.mkPrimitiveCast( + 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 view = arrayView(collection, type) + val size = if (method.name == "cap") view.capacity else view.length + checkLength(size) ?: throw GoStepAbort() + size + } + + is MapType -> { + scope.ensureObjectMapSizeCorrect(collection, type) ?: throw GoStepAbort() + symbolicObjectMapSize(collection, type) + } + + else -> { + error("Inconsistent Go interpreter state") + } + } + } + }, + falseBranch = { + ctx.mkSizeExpr(0) + }, + ), + ctx.typeToSort(returnType) + ) + } + + "panic" -> { + val value = args[0].accept(this) + scope.calcOnState { + panic(value, args[0].type) + ctx.noValue + } + } + + "recover" -> { + scope.calcOnState { + recover() + } + } + + else -> { + unsupportedExpr("builtin ${method.name}") + } + } + } + + private fun copyMap( + srcMap: UHeapRef, + mapType: MapType, + ): UHeapRef = with(ctx) { + checkNotNull(srcMap) ?: throw GoStepAbort() + scope.ensureObjectMapSizeCorrect(srcMap, mapType) ?: throw GoStepAbort() + + 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 { + symbolicPrimitiveMapCopyIntoEmpty(destMap, srcMap, mapType, keySort, valueSort, USizeExprKeyInfo()) + } + destMap + } + } + + private fun compareStrings(expr: GoBinaryExpr): UExpr { + val left = unboxNamedRef(expr.lhv.accept(this).asExpr(ctx.addressSort), expr.lhv.type) + val right = unboxNamedRef(expr.rhv.accept(this).asExpr(ctx.addressSort), expr.rhv.type) + return scope.calcOnState { + val leftView = arrayView(left, GoBasicTypes.STRING) + val rightView = arrayView(right, GoBasicTypes.STRING) + val bound = (leftView.length as? KBitVec32Value)?.intValue + ?: (rightView.length as? KBitVec32Value)?.intValue + ?: throw UnsupportedOperationException("String comparison with two symbolic lengths is not supported") + require(bound >= 0) { "Negative string length" } + var equalPrefix: UExpr = ctx.trueExpr + var less: UExpr = ctx.falseExpr + for (index in 0 until bound) { + val position = ctx.mkSizeExpr(index) + val inBoth = ctx.mkAnd( + ctx.mkSizeLtExpr(position, leftView.length), + ctx.mkSizeLtExpr(position, rightView.length) + ) + val leftByte = memory.readGoArrayIndex( + leftView.backing, + ctx.mkSizeAddExpr(leftView.offset, position), + leftView.storageType, + ctx.bv8Sort + ) + val rightByte = memory.readGoArrayIndex( + rightView.backing, + ctx.mkSizeAddExpr(rightView.offset, position), + rightView.storageType, + ctx.bv8Sort + ) + less = ctx.mkOr(less, ctx.mkAnd(equalPrefix, inBoth, ctx.mkBvUnsignedLessExpr(leftByte, rightByte))) + equalPrefix = ctx.mkAnd(equalPrefix, ctx.mkOr(ctx.mkNot(inBoth), ctx.mkEq(leftByte, rightByte))) + } + less = ctx.mkOr(less, ctx.mkAnd(equalPrefix, ctx.mkSizeLtExpr(leftView.length, rightView.length))) + val equal = ctx.mkAnd(equalPrefix, ctx.mkEq(leftView.length, rightView.length)) + when (expr) { + is GoEqlExpr -> { + equal + } + is GoNeqExpr -> { + ctx.mkNot(equal) + } + is GoLssExpr -> { + less + } + is GoLeqExpr -> { + ctx.mkOr(less, equal) + } + is GoGtrExpr -> { + ctx.mkNot(ctx.mkOr(less, equal)) + } + is GoGeqExpr -> { + ctx.mkNot(less) + } + else -> { + error("Unsupported string operation: $expr") + } + } + } + } + + private fun appendArray(sliceValue: GoValue, appendValue: GoValue): UExpr { + val slice = unboxNamedRef(sliceValue.accept(this).asExpr(ctx.addressSort), sliceValue.type) + val appended = unboxNamedRef(appendValue.accept(this).asExpr(ctx.addressSort), appendValue.type) + val type = sliceValue.type.underlying() + val isString = type == GoBasicTypes.STRING + val elementSort = if (isString) ctx.bv8Sort else ctx.typeToSort((type as SliceType).elementType) + val zero = ctx.mkSizeExpr(0) + return scope.calcOnState { + val view = arrayView(slice, type) + val appendedView = arrayView(appended, appendValue.type) + val length = ctx.mkSizeAddExpr(view.length, appendedView.length) + checkLength(length) ?: throw GoStepAbort() + val newBacking = memory.allocateGoArray(type, ctx.sizeSort, length) + memory.copyGoArray(view.backing, newBacking, view.storageType, elementSort, view.offset, zero, view.length) + memory.copyGoArray( + appendedView.backing, + newBacking, + view.storageType, + elementSort, + appendedView.offset, + view.length, + appendedView.length + ) + if (isString) return@calcOnState newBacking + + val reuse = ctx.mkSizeLeExpr(length, view.capacity) + val backing = ctx.mkIte(reuse, view.backing, newBacking) + val offset = ctx.mkIte(reuse, view.offset, zero) + memory.copyGoArray( + appendedView.backing, + backing, + view.storageType, + elementSort, + appendedView.offset, + ctx.mkSizeAddExpr(offset, view.length), + appendedView.length + ) + val header = memory.allocateGoArray(type, ctx.sizeSort, length) + data.arrayViews[header] = GoArrayView( + backing, view.storageType, offset = offset, + length = length, capacity = ctx.mkIte(reuse, view.capacity, length) + ) + header + } + } + + 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") + } +} 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..417054a5e8 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -0,0 +1,213 @@ +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.GoStepAbort +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 GoStepAbort() + scope.ensureObjectMapSizeCorrect(map, type) ?: throw GoStepAbort() + + 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) + + val keyIsNew = ctx.mkNot(memory.read(mapContainsLValue)) + + 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, keyIsNew) + } + + 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..9158fd9ad1 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt @@ -0,0 +1,189 @@ +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 + +internal val logger = object : KLogging() {}.logger + +class GoMachine( + private val program: GoProgram, + override 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, intSize = program.intSize) + 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)) + } + + @Suppress("LongMethod") // Observer and path-selection setup follows UMachine. + private fun analyze(methodsList: List, targets: List = emptyList()): List { + logger.debug("{}.analyze()", this) + val unsupported = methodsList.filter { it.hasUnsupportedInstructions() }.map { it.metName } + if (unsupported.isNotEmpty()) { + throw UnsupportedOperationException("Methods contain unsupported instructions: $unsupported") + } + val methods = methodsList + 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 + ) + ) + } + + interpreter.unsupportedOperations.clear() + run( + interpreter, + pathSelector, + observer = CompositeUMachineObserver(observers), + isStateTerminated = ::isStateTerminated, + stopStrategy = stopStrategy, + ) + + interpreter.unsupportedOperations.firstOrNull()?.let { throw it } + + logger.debug("Total coverage: ${coverageStatistics.getTotalCoverage().roundToInt()}%") + for (method in coverageStatistics.coverageZone) { + logger.debug( + "Method ${method.metName} coverage: ${coverageStatistics.getMethodCoverage(method).roundToInt()}%" + ) + } + val methodsAllowPartialCoverage = customOptions.uncoveredMethods.containsAll(methods.map { it.metName }) + if (coverageStatistics.getTotalCoverage() < FULL_COVERAGE && !methodsAllowPartialCoverage) { + check(!customOptions.failOnNotFullCoverage) { "coverage not 100%" } + logger.warn("Analysis of methods ${methods.map { it.metName }} reached coverage < 100%") + } + + return statesCollector.collectedStates + } + + private companion object { + const val FULL_COVERAGE = 100.0 + } + + 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..583cde1f23 --- /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, +) 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..0c447f03b1 --- /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 + +open class GoTarget( + location: GoInst, +) : UTarget(location) 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..04755a5e76 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt @@ -0,0 +1,142 @@ +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() { + internal val unsupportedOperations: MutableList = mutableListOf() + + @Suppress("NestedBlockDepth") // Initializes pointer chains for package globals. + 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++ + } + repeat(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) + + try { + val nextInst = next(state, inst, instVisitor) + if (nextInst !is GoNullInst) { + state.newInst(nextInst) + } + } catch (_: GoStepAbort) { + // The scope already records the surviving panic or forked states. + } catch (error: UnsupportedOperationException) { + unsupportedOperations += error + throw error + } + 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 checkNotNull( + function.recover + ) { "Recovered function has no recovery block" }.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/GoStepAbort.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoStepAbort.kt new file mode 100644 index 0000000000..117ed81330 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoStepAbort.kt @@ -0,0 +1,4 @@ +package org.usvm.interpreter + +/** Stops the current hop when StepScope has forked to a panic or discarded an infeasible state. */ +internal class GoStepAbort : RuntimeException("Current Go step cannot continue", null, false, false) 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..367927021a --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -0,0 +1,556 @@ +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.SignatureType +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.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.arrayView +import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.memory.readGoArrayIndex +import org.usvm.mkSizeAddExpr +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 -> { + val value = resolveBv8(expr) + if (baseType == GoBasicTypes.UINT8) value.toUByte() else value + } + is KBv16Sort -> { + val value = resolveBv16(expr) + if (baseType == GoBasicTypes.UINT16) value.toUShort() else value + } + is KBv32Sort -> { + val value = resolveBv32(expr) + when (baseType) { + GoBasicTypes.UINT, GoBasicTypes.UINT32, GoBasicTypes.UINTPTR -> value.toUInt() + GoBasicTypes.RUNE -> Char(value) + else -> value + } + } + is KBv64Sort -> { + val value = resolveBv64(expr) + val unsigned = baseType == GoBasicTypes.UINT || + baseType == GoBasicTypes.UINT64 || baseType == GoBasicTypes.UINTPTR + if (unsigned) value.toULong() else value + } + is KFp32Sort -> { + resolveFp32(expr) + } + is KFp64Sort -> { + resolveFp64(expr) + } + is UAddressSort -> { + resolveReference(expr.asExpr(ctx.addressSort), baseType) + } + + else -> { + Any() + } + } + + private fun resolveReference(reference: UHeapRef, baseType: GoType): Any? { + val type = baseType.underlying() + if (baseType is NamedType) return resolveBoxed(reference, type) + return when (type) { + GoBasicTypes.STRING -> { + resolveString(reference, type) + } + is BasicType -> { + resolveBoxed(reference, type) + } + is ArrayType -> { + resolveArray(reference, type, type.len, type.elementType) + } + is SliceType -> { + resolveSlice(reference, type, type.elementType) + } + is MapType -> { + resolveMap(reference, type, type.keyType, type.valueType) + } + is TupleType -> { + resolveTuple(reference, type) + } + is StructType -> { + resolveStruct(reference, type) + } + is InterfaceType -> { + resolveInterface(reference) + } + is NullType -> { + null + } + is PointerType -> { + resolvePointer(reference, type.baseType) + } + is SignatureType -> { + GoFunctionReference(reference.toString()) + } + else -> { + error("Cannot resolve Go type: $type") + } + } + } + + 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 view = state.arrayView(string, arrayType) + val lengthUExpr = view.length + 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 view = state.arrayView(array, arrayType) + val length = clipArrayLength(len.toInt()) + val sort = typeToSort(elementType) + return List(length) { idx -> + val element = memory.readGoArrayIndex( + view.backing, + mkBvAddExpr(view.offset, mkSizeExpr(idx)), + view.storageType, + 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 view = state.arrayView(slice, sliceType) + val lengthUExpr = view.length + val length = clipArrayLength(resolveSize(lengthUExpr)) + val sort = typeToSort(elementType) + return List(length) { idx -> + val offset = view.offset + val index = ctx.mkBvAddExpr(offset, mkSizeExpr(idx)) + val element = memory.readGoArrayIndex( + view.backing, + index, + view.storageType, + sort + ) + convertExpr(element, elementType) + } + } + + @Suppress("NestedBlockDepth") // Resolves both primitive and reference map regions. + 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 -> { + error("Inconsistent Go interpreter state") + } + } + + 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 target = state.data.pointerTargets[pointer] + val expr = if (target == null) { + memory.readField( + pointer, + index, + ctx.typeToSort(baseType) + ) + } else { + memory.read(target) + } + return "&" + convertExpr(expr, baseType) + } + + fun resolveBoxed(value: UHeapRef, type: GoType): Any? = with(ctx) { + if (value == mkConcreteHeapRef(NULL_ADDRESS) || value == nullRef) { + 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( + val arguments: List, +) { + override fun toString(): String { + return buildString { + appendLine("InputModel") + val arguments = arguments.joinToString(", ", "Arguments [", "]") + appendLine(arguments.prependIndent("\t")) + } + } +} + +class OutputModel( + val returnExpr: Any?, +) { + override fun toString(): String { + return buildString { + appendLine("OutputModel") + val returnString = "Return [$returnExpr]" + appendLine(returnString.prependIndent("\t")) + } + } +} + +class SuccessfulExecutionResult( + val inputModel: InputModel, + 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( + 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 -> { + error("Inconsistent Go interpreter state") + } + } + + fun generateUniqueMapKey(map: Map): Any? { + for (i in 0 until maxAttempts) { + val key = generate() + if (!map.containsKey(key)) { + return key + } + } + + return null + } +} + +/** A function supplied to a mocked call; it cannot currently be replayed as a concrete Go function. */ +data class GoFunctionReference(val expression: String) 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..5f7465f4fa --- /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.arrayStorageType(), sizeSort, count) + return reference +} + +internal fun UWritableMemory.allocateGoArrayInitialized( + type: GoType, + sort: Sort, + sizeSort: SizeSort, + contents: Sequence>, +): UConcreteHeapRef { + val reference = allocConcrete(type) + initializeArray(reference, type.arrayStorageType(), sort, sizeSort, contents) + return reference +} diff --git a/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayMemory.kt b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayMemory.kt new file mode 100644 index 0000000000..a320b02598 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayMemory.kt @@ -0,0 +1,68 @@ +package org.usvm.memory + +import org.jacodb.go.api.ArrayType +import org.jacodb.go.api.GoType +import org.jacodb.go.api.SliceType +import org.usvm.UBoolExpr +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.USort +import org.usvm.api.memcpy +import org.usvm.api.readArrayIndex +import org.usvm.api.readArrayLength +import org.usvm.api.writeArrayIndex +import org.usvm.api.writeArrayLength +import org.usvm.type.GoBasicTypes +import org.usvm.type.underlying + +// Arrays, slices and strings share storage by element type. Headers retain their Go type. +internal fun GoType.arrayStorageType(): GoType = when (val type = underlying()) { + is ArrayType -> { + SliceType(type.elementType) + } + GoBasicTypes.STRING -> { + SliceType(GoBasicTypes.UINT8) + } + else -> { + type + } +} + +internal fun UReadOnlyMemory<*>.readGoArrayIndex( + reference: UHeapRef, + index: UExpr, + type: GoType, + sort: Sort, +): UExpr = readArrayIndex(reference, index, type.arrayStorageType(), sort) + +internal fun UReadOnlyMemory<*>.readGoArrayLength( + reference: UHeapRef, + type: GoType, + sizeSort: SizeSort, +): UExpr = readArrayLength(reference, type.arrayStorageType(), sizeSort) + +internal fun UWritableMemory<*>.writeGoArrayIndex( + reference: UHeapRef, + index: UExpr, + type: GoType, + sort: Sort, + value: UExpr, + guard: UBoolExpr, +) = writeArrayIndex(reference, index, type.arrayStorageType(), sort, value, guard) + +internal fun UWritableMemory<*>.writeGoArrayLength( + reference: UHeapRef, + length: UExpr, + type: GoType, + sizeSort: SizeSort, +) = writeArrayLength(reference, length, type.arrayStorageType(), sizeSort) + +internal fun UWritableMemory<*>.copyGoArray( + source: UHeapRef, + destination: UHeapRef, + type: GoType, + sort: Sort, + sourceOffset: UExpr, + destinationOffset: UExpr, + length: UExpr, +) = memcpy(source, destination, type.arrayStorageType(), sort, sourceOffset, destinationOffset, length) diff --git a/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt new file mode 100644 index 0000000000..1e47b54d5a --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt @@ -0,0 +1,43 @@ +package org.usvm.memory + +import io.ksmt.expr.KIteExpr +import org.jacodb.go.api.GoType +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.USizeSort +import org.usvm.mkSizeExpr +import org.usvm.sizeSort +import org.usvm.state.GoState + +internal data class GoArrayView( + val backing: UHeapRef, + val storageType: GoType, + val offset: UExpr, + val length: UExpr, + val capacity: UExpr, +) + +internal fun GoState.arrayView(reference: UHeapRef, type: GoType): GoArrayView { + data.arrayViews[reference]?.let { return it } + + if (reference is KIteExpr) { + val positive = arrayView(reference.trueBranch, type) + val negative = arrayView(reference.falseBranch, type) + check(positive.storageType == negative.storageType) { "Array view storage types differ" } + return GoArrayView( + ctx.mkIte(reference.condition, positive.backing, negative.backing), + positive.storageType, + offset = ctx.mkIte(reference.condition, positive.offset, negative.offset), + length = ctx.mkIte(reference.condition, positive.length, negative.length), + capacity = ctx.mkIte(reference.condition, positive.capacity, negative.capacity), + ) + } + + val zero = ctx.mkSizeExpr(0) + val length = ctx.mkIte( + ctx.mkHeapRefEq(reference, ctx.nullRef), + trueBranch = { zero }, + falseBranch = { memory.readGoArrayLength(reference, type, ctx.sizeSort) }, + ) + return GoArrayView(reference, type.arrayStorageType(), offset = zero, length = length, capacity = length) +} 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..8bcdd44a25 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt @@ -0,0 +1,750 @@ +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 + +@Suppress("LargeClass") // Conversion covers the complete serialized SSA schema. +class Converter private constructor() { + companion object { + fun unpack(pkg: Package): GoPackage = Converter().unpackPackage(pkg) + } + + private val typesMap: MutableMap = mutableMapOf() + + private fun unpackPackage(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.toMap(), intSize = pkg.intSize) + } + + 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) }) + } + + @Suppress("LongMethod") // Exhaustive SSA instruction dispatch. + 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 ::GoAndExpr, + "&^" to ::GoAndNotExpr, + "||" to ::GoOrExpr, + "|" 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.getValue(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.getValue(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 (value.value.value == "nil") return GoNullConstant(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( + requireNotNull(const.bytes) { "String constants require exported bytes" }, + 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.getValue(type) + } + return unpack(types, types.getValue(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.getValue(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 } + } +} 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..125026b614 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt @@ -0,0 +1,644 @@ +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, + @SerialName("int_size") val intSize: Int, +) + +@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 bytes: String? = null, + 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 +} 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..e74c575095 --- /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) + } +} 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..256b86f05c --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt @@ -0,0 +1,141 @@ +package org.usvm.operator + +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.type.goCtx + +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, shift) } + ) + + class Shr(signed: Boolean) : GoBinaryOperator( + onBv = { arg, shift -> + if (signed) { + mkBvArithShiftRightExpr(arg, shift) + } else { + mkBvLogicalShiftRightExpr(arg, shift) + } + } + ) + + data object AndNot : GoBinaryOperator( + onBv = { lhs, rhs -> mkBvAndExpr(lhs, mkBvNotExpr(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") } + } +} 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..efa83c80e2 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt @@ -0,0 +1,82 @@ +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 + ) + ) + ) + ) + } 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..6bc34f2661 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt @@ -0,0 +1,85 @@ +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.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 Complement : GoUnaryOperator( + onBv = GoContext::mkBvNotExpr, + ) + + data object CastToBool : GoUnaryOperator( + onBool = { it }, + onBv = { operand -> operand neq mkBv(0, operand.sort) } + ) + + data object CastToInt8 : GoUnaryOperator( + onBool = { operand -> operand.wideTo32BitsIfNeeded(signed = false) }, + onBv = { operand -> operand.mkNarrow(Byte.SIZE_BITS, signed = true) } + ) + + data object CastToInt16 : GoUnaryOperator( + onBool = { operand -> operand.wideTo32BitsIfNeeded(signed = false) }, + onBv = { operand -> operand.mkNarrow(Short.SIZE_BITS, signed = true) } + ) + + data object CastToInt32 : GoUnaryOperator( + 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(signed = 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") } + } +} 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..da983c2035 --- /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 +} 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..966a5594c8 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -0,0 +1,416 @@ +package org.usvm.state + +import io.ksmt.expr.KBitVec32Value +import io.ksmt.expr.KConst +import io.ksmt.expr.KIteExpr +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.UNullRef +import org.usvm.USort +import org.usvm.UState +import org.usvm.api.readField +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.memory.allocateGoArray +import org.usvm.memory.allocateGoArrayInitialized +import org.usvm.memory.writeGoArrayLength +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, + cloneOwnership, + 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 null + + 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.writeGoArrayLength(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 { + if (pointer is KIteExpr) { + return ctx.mkIte( + pointer.condition, + deref(pointer.trueBranch, sort).asExpr(sort), + deref(pointer.falseBranch, sort).asExpr(sort) + ) + } + val target = data.pointerTargets[pointer] + return if (target != null) memory.read(target) else memory.readField(pointer, 0, sort) + } + + fun store(pointer: UHeapRef, rvalue: UExpr) { + storeGuarded(pointer, rvalue, ctx.trueExpr) + } + + private fun storeGuarded(pointer: UHeapRef, rvalue: UExpr, guard: UExpr) { + if (pointer is KIteExpr) { + storeGuarded(pointer.trueBranch, rvalue, ctx.mkAnd(guard, pointer.condition)) + storeGuarded(pointer.falseBranch, rvalue, ctx.mkAnd(guard, ctx.mkNot(pointer.condition))) + return + } + val target = data.pointerTargets[pointer] + if (target != null) { + memory.write(target.withSort(rvalue.sort), rvalue.asExpr(rvalue.sort), guard) + } else { + memory.writeField(pointer, 0, rvalue.sort, rvalue.asExpr(rvalue.sort), guard) + } + } + + 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 { + val isReference = expr.sort == ctx.addressSort && expr !is UNullRef && expr !is KConst<*> + if (isReference && 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 { + data.pointerTargets.entries.firstOrNull { it.value == lvalue }?.let { return it.key } + val pointer = memory.allocConcrete(PointerType(type)) + memory.writeField(pointer, 1, ctx.bv32Sort, ctx.mkBv(POINTER_FIELD, ctx.bv32Sort), ctx.trueExpr) + data.pointerTargets[pointer] = lvalue + return pointer + } + + 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 = mkString(value.toByteArray(Charsets.UTF_8)) + + fun mkString(value: ByteArray): UExpr { + return memory.allocateGoArrayInitialized( + GoBasicTypes.STRING, + ctx.bv8Sort, + ctx.sizeSort, + value.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.allocateGoArrayInitialized(type, sort, ctx.sizeSort, contents) + } + + is SliceType -> { + memory.allocateGoArray(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 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() + } + + + error("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 = 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 new file mode 100644 index 0000000000..ed5d572038 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt @@ -0,0 +1,58 @@ +package org.usvm.state + +import org.jacodb.go.api.GoMethod +import org.usvm.GoCall +import org.usvm.UHeapRef +import org.usvm.memory.GoArrayView +import org.usvm.memory.ULValue + +enum class GoFlowStatus { + NORMAL, + PANIC, + DEFER, +} + +class GoStateData( + val flowStack: ArrayDeque = ArrayDeque(), +) { + internal val arrayViews: MutableMap = hashMapOf() + + internal val pointerTargets: MutableMap> = hashMapOf() + + 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) { + deferredCalls.getOrPut(method) { ArrayDeque() }.addLast(call) + } + + fun clone(): GoStateData = GoStateData(clonedFlowStack()).also { + it.pointerTargets.putAll(pointerTargets) + it.arrayViews.putAll(arrayViews) + for ((method, calls) in deferredCalls) { + calls.forEach { call -> it.addDeferredCall(method, call) } + } + } + + fun mergeWith(other: GoStateData): GoStateData? { + val thisCalls = deferredCalls.mapValues { it.value.toList() } + val otherCalls = other.deferredCalls.mapValues { it.value.toList() } + val sameCalls = thisCalls == otherCalls + if (flowStack.toList() != other.flowStack.toList() || !sameCalls || + arrayViews != other.arrayViews || pointerTargets != other.pointerTargets + ) { + return null + } + return clone() + } + + private fun clonedFlowStack(): ArrayDeque { + val newStack = ArrayDeque() + newStack.addAll(flowStack) + return newStack + } +} 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..527c8f0f61 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt @@ -0,0 +1,95 @@ +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.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 = BasicType("string") + 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 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..7598b7cebb --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt @@ -0,0 +1,108 @@ +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 + } + } +} 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..891f094d36 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/util/Extensions.kt @@ -0,0 +1,30 @@ +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/GoSemanticRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/GoSemanticRegressionTest.kt new file mode 100644 index 0000000000..3130378f10 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/GoSemanticRegressionTest.kt @@ -0,0 +1,115 @@ +package org.usvm + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.usvm.interpreter.SuccessfulExecutionResult +import org.usvm.interpreter.UnsuccessfulExecutionResult +import org.usvm.model.Converter +import org.usvm.model.Parser +import java.io.File +import java.nio.file.Files +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class GoSemanticRegressionTest { + @TestFactory + fun replayGeneratedInputsWithNativeGo(): Collection = listOf( + "symbolicBranch" to 3, + "symbolicSliceAlias" to 2, + ).map { (name, count) -> DynamicTest.dynamicTest(name) { replayInputs(name, expectedExecutions = count) } } + + private fun replayInputs(methodName: String, expectedExecutions: Int) { + val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("regressions/usvm_regressions.json").path)) + val options = UMachineOptions(timeout = 5.seconds, solverTimeout = 2.seconds, typeOperationsTimeout = 2.seconds) + val customOptions = GoMachineOptions(failOnNotFullCoverage = true, uncoveredMethods = emptyList()) + val executions = GoMachine(GoProgram(listOf(pkg)), options, customOptions).use { machine -> + machine.analyzeAndResolve(pkg, methodName).map { assertIs(it) } + } + assertEquals(expectedExecutions, executions.size, message = "One witness per branch") + val inputs = executions.map { (it.inputModel.arguments.single() as Number).toLong() } + if (methodName == "symbolicBranch") { + assertTrue(inputs.any { it < 0 } && inputs.any { it == 0L } && inputs.any { it > 0 }) + } + + val replayFile = Files.createTempFile("usvm-go-replay-", ".json").toFile() + val logFile = Files.createTempFile("usvm-go-replay-", ".log").toFile() + try { + replayFile.writeText(inputs.joinToString(prefix = "[", postfix = "]")) + val builder = ProcessBuilder( + generatedGoFile("native-replay.test").path, + "-test.run=^TestReplaySymbolicInputs$" + ) + .redirectErrorStream(true).redirectOutput(logFile) + builder.environment()["USVM_GO_REPLAY_FILE"] = replayFile.path + builder.environment()["USVM_GO_REPLAY_METHOD"] = methodName + val process = builder.start() + try { + assertTrue(process.waitFor(5, TimeUnit.SECONDS), "Native replay timed out") + assertEquals(expected = 0, actual = process.exitValue(), message = logFile.readText()) + } finally { + process.destroyForcibly() + } + val nativeOutputs = Json.parseToJsonElement(replayFile.readText()).jsonArray.map { it.jsonPrimitive.int } + val symbolicOutputs = executions.map { (it.outputModel.returnExpr as Number).toInt() } + assertEquals(nativeOutputs, symbolicOutputs, "Replay of generated inputs") + } finally { + replayFile.delete() + logFile.delete() + } + } + + @TestFactory + fun compareWithNativeGo(): Collection { + val nativeResults = Json.parseToJsonElement( + File(generatedGoFile("native-oracle.json").path).readText() + ).jsonObject + val pkg = Converter.unpack( + Parser().deserialize(filename = generatedGoFile("regressions/usvm_regressions.json").path) + ) + val expectedNames = pkg.methods + .filter { it.parameters.isEmpty() && it.metName != "init" } + .map { it.metName }.toSet() + assertEquals(expectedNames, nativeResults.keys, "Every zero-argument fixture must have a native oracle") + val program = GoProgram(listOf(pkg)) + val options = UMachineOptions( + pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), + coverageZone = CoverageZone.TRANSITIVE, + exceptionsPropagation = true, + timeout = 5.seconds, + solverTimeout = 2.seconds, + typeOperationsTimeout = 2.seconds, + ) + + return nativeResults.map { (name, nativeResult) -> + DynamicTest.dynamicTest(name) { + val expected = nativeResult.jsonPrimitive.content + val customOptions = GoMachineOptions( + failOnNotFullCoverage = expected != "panic", + uncoveredMethods = emptyList(), + ) + + val actual = GoMachine(program, options, customOptions).use { machine -> + val executions = machine.analyzeAndResolve(pkg, name) + assertEquals(expected = 1, actual = executions.size, message = "Expected one complete execution") + val execution = executions.single() + if (expected == "panic") { + assertIs(execution) + "panic" + } else { + assertIs(execution).outputModel.returnExpr.toString() + } + } + + assertEquals(expected, actual, message = "Native Go result for $name") + } + } + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/GoTestFixtures.kt b/usvm-go/src/test/kotlin/org/usvm/GoTestFixtures.kt new file mode 100644 index 0000000000..c7b6eadc79 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/GoTestFixtures.kt @@ -0,0 +1,5 @@ +package org.usvm + +import java.io.File + +internal fun generatedGoFile(name: String): File = File(System.getProperty("usvm.go.generatedDir"), name) diff --git a/usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.kt b/usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.kt new file mode 100644 index 0000000000..9a572a1ba1 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.kt @@ -0,0 +1,30 @@ +package org.usvm + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.usvm.model.Converter +import org.usvm.model.Parser +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class GoUnsupportedTest { + @Test + fun goroutinesAreRejectedAtEntry() = assertUnsupported("unsupportedGoroutine", "unsupported instructions") + + @Test + fun goroutinesAreRejectedInsideCalls() = assertUnsupported("unsupportedCaller", "unsupported instructions") + + @Test + fun twoSymbolicStringLengthsAreRejected() = assertUnsupported("symbolicStringEquality", "two symbolic lengths") + + private fun assertUnsupported(method: String, message: String) { + val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("regressions/usvm_regressions.json").path)) + val options = UMachineOptions(timeout = 5.seconds, solverTimeout = 2.seconds, typeOperationsTimeout = 2.seconds) + val customOptions = GoMachineOptions(failOnNotFullCoverage = true, uncoveredMethods = emptyList()) + + GoMachine(GoProgram(listOf(pkg)), options, customOptions).use { machine -> + val error = assertThrows { machine.analyzeAndResolve(pkg, method) } + assertTrue(error.message.orEmpty().contains(message), error.message) + } + } +} 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..11199fe194 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt @@ -0,0 +1,46 @@ +package org.usvm + +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.TestFactory +import org.usvm.model.Converter +import org.usvm.model.Parser +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +/** Coverage and execution smoke tests; scalar semantics are checked by GoSemanticRegressionTest. */ +class JacoDbTest { + @TestFactory + fun fastSamples(): Collection = samples(slow = false) + + @Tag("manual") + @TestFactory + fun slowSamples(): Collection = samples(slow = true) + + private fun samples(slow: Boolean): Collection { + val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("examples/usvm_examples.json").path)) + val program = GoProgram(listOf(pkg)) + val options = UMachineOptions( + pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), + coverageZone = CoverageZone.TRANSITIVE, + exceptionsPropagation = true, + timeout = (if (slow) 30 else 5).seconds, + solverTimeout = 2.seconds, + typeOperationsTimeout = 2.seconds, + ) + val customOptions = GoMachineOptions( + failOnNotFullCoverage = true, + uncoveredMethods = listOf("panicRecoverComplex"), + ) + return pkg.methods.filter { '$' !in it.metName && (it.metName in slowMethods) == slow }.map { method -> + DynamicTest.dynamicTest(method.metName) { + GoMachine(program, options, customOptions).use { machine -> + val results = machine.analyzeAndResolve(pkg, method.metName) + assertTrue(results.isNotEmpty(), "No complete execution for ${method.metName}") + } + } + } + } + + private val slowMethods = setOf("loopInfinite", "loopInner", "loopCollatz", "mapLoopLen", "canVisitAllRooms") +} 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..8028cc7df4 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt @@ -0,0 +1,22 @@ +package org.usvm.model + +import org.junit.jupiter.api.Test +import org.usvm.generatedGoFile +import java.io.File +import kotlin.test.assertEquals + +class ModelTest { + @Test + fun testEqualContent() { + val filename = generatedGoFile("examples/usvm_examples.json").path + val file = File(filename) + + val parser = Parser() + val pkg = parser.deserialize(filename) + + val expected = file.readText() + val actual = parser.serialize(pkg) + + assertEquals(expected, actual) + } +} 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 From e15a2d0e6a9685d05e7a110fc615842fca80033f Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Fri, 9 Oct 2026 13:55:32 +0300 Subject: [PATCH 2/6] Organize Go tests around symbolic semantic expectations Replace the original coverage smoke factories with explicit categorized properties using the shared TestRunner. Require both discovery of expected branches and consistency of every collected execution. Preserve native oracle comparisons and witness replay, expose structured pointer/interface values and argument snapshots, and keep the newly detected frontend/model failures visible in the draft integration. --- usvm-go/README.md | 82 ++++- usvm-go/build.gradle.kts | 1 + usvm-go/src/main/go/regressions/semantics.go | 5 + .../src/main/go/regressions/semantics_test.go | 1 + .../org/usvm/interpreter/GoTestInterpreter.kt | 27 +- .../org/usvm/GoSemanticRegressionTest.kt | 115 ------ .../test/kotlin/org/usvm/GoUnsupportedTest.kt | 30 -- .../src/test/kotlin/org/usvm/JacoDbTest.kt | 46 --- .../org/usvm/samples/GoExpectedValues.kt | 83 +++++ .../org/usvm/samples/GoMethodTestRunner.kt | 348 ++++++++++++++++++ .../org/usvm/samples/GoNativeTestRunner.kt | 65 ++++ .../test/kotlin/org/usvm/samples/GoSample.kt | 5 + .../org/usvm/samples/SampleCoverageTest.kt | 72 ++++ .../usvm/samples/algorithms/AlgorithmsTest.kt | 33 ++ .../samples/algorithms/SlowAlgorithmsTest.kt | 25 ++ .../arithmetic/ArithmeticRegressionTest.kt | 63 ++++ .../samples/arrays/ArrayRegressionTest.kt | 11 + .../org/usvm/samples/arrays/ArraysTest.kt | 47 +++ .../org/usvm/samples/calls/CallsTest.kt | 44 +++ .../collections/maps/MapAllocationTest.kt | 35 ++ .../collections/maps/MapIterationTest.kt | 25 ++ .../samples/collections/maps/MapLookupTest.kt | 96 +++++ .../collections/maps/MapMutationTest.kt | 82 +++++ .../collections/maps/MapRegressionTest.kt | 32 ++ .../collections/maps/SlowMapIterationTest.kt | 33 ++ .../collections/slices/SliceAlgorithmsTest.kt | 93 +++++ .../collections/slices/SliceAllocationTest.kt | 79 ++++ .../collections/slices/SliceMutationTest.kt | 70 ++++ .../collections/slices/SliceRegressionTest.kt | 71 ++++ .../collections/slices/SliceViewsTest.kt | 138 +++++++ .../slices/SymbolicSliceAliasTest.kt | 22 ++ .../samples/controlflow/ConditionsTest.kt | 99 +++++ .../org/usvm/samples/controlflow/LoopsTest.kt | 49 +++ .../usvm/samples/controlflow/SlowLoopsTest.kt | 47 +++ .../samples/controlflow/SymbolicBranchTest.kt | 23 ++ .../org/usvm/samples/exceptions/DeferTest.kt | 72 ++++ .../org/usvm/samples/globals/GlobalsTest.kt | 56 +++ .../org/usvm/samples/objects/ObjectsTest.kt | 88 +++++ .../org/usvm/samples/pointers/PointersTest.kt | 50 +++ .../samples/runner/GoMethodTestRunnerTest.kt | 61 +++ .../serialization}/ModelTest.kt | 3 +- .../usvm/samples/strings/NamedStringsTest.kt | 39 ++ .../samples/strings/StringConstraintsTest.kt | 92 +++++ .../samples/strings/StringRegressionTest.kt | 47 +++ .../org/usvm/samples/strings/StringsTest.kt | 41 +++ .../org/usvm/samples/types/InterfacesTest.kt | 118 ++++++ .../org/usvm/samples/types/NamedTypesTest.kt | 81 ++++ .../org/usvm/samples/types/StructsTest.kt | 100 +++++ .../samples/unsupported/GoUnsupportedTest.kt | 35 ++ usvm-go/src/test/resources/logback.xml | 2 +- 50 files changed, 2771 insertions(+), 211 deletions(-) delete mode 100644 usvm-go/src/test/kotlin/org/usvm/GoSemanticRegressionTest.kt delete mode 100644 usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.kt delete mode 100644 usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/GoExpectedValues.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/GoMethodTestRunner.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/GoSample.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/algorithms/AlgorithmsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/algorithms/SlowAlgorithmsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArrayRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArraysTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/calls/CallsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapAllocationTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapIterationTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapLookupTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapMutationTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAlgorithmsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAllocationTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceMutationTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceViewsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SymbolicSliceAliasTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/controlflow/ConditionsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/controlflow/LoopsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SlowLoopsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SymbolicBranchTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/globals/GlobalsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/objects/ObjectsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointersTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/runner/GoMethodTestRunnerTest.kt rename usvm-go/src/test/kotlin/org/usvm/{model => samples/serialization}/ModelTest.kt (87%) create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/strings/NamedStringsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/strings/StringConstraintsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/strings/StringRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/strings/StringsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/types/NamedTypesTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/types/StructsTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt diff --git a/usvm-go/README.md b/usvm-go/README.md index 343be952ba..189c5c5ffe 100644 --- a/usvm-go/README.md +++ b/usvm-go/README.md @@ -21,25 +21,83 @@ The test task generates SSA JSON and the native oracle under `usvm-go/build/gene No checked-in dumps or pre-existing `out` directory are required. Tests have finite machine/solver timeouts, close machines, and are bounded by a 15-minute Gradle timeout. -`GoSemanticRegressionTest` compares zero-argument scalar results and panic outcomes -with a native Go execution. It also asks USVM for branch witnesses, then executes -those concrete inputs in Go, including a write through an aliased slice after a fork. -The scalar cases cover shifts, bit operations, native integer width and unsigned -widening, slice length/capacity, aliasing, copy/append, array pointers, map size, -string byte content/order/conversion, and selected panic paths. +## Test organization and symbolic expectations -`GoUnsupportedTest` checks that goroutines and comparison of two strings with -symbolic lengths produce explicit unsupported results. `JacoDbTest` retains the -98 fast prototype examples as execution/coverage smoke checks; these do not assert -native output values. `panicRecoverComplex` remains an explicit partial-coverage -exception. `ModelTest` checks SSA JSON round-tripping. +Tests follow the Java frontend's `TestRunner` pattern under `org.usvm.samples`: -The five slow prototype examples are manual and bounded: +- `arithmetic`, `arrays`, `collections.maps`, `collections.slices`, `strings`; +- `controlflow`, `calls`, `exceptions`, `globals`, `objects`, `pointers`, `types`, `algorithms`; +- `unsupported`, `serialization`, and `runner` for infrastructure contracts. + +All 103 original examples have explicit tests, including the five manual examples. +`SampleCoverageTest` checks that every exported method is registered exactly once, +and that each zero-argument regression has an independently generated native oracle. + +`GoMethodTestRunner` reuses the shared `TestRunner` with `checkDiscoveredProperties` +and `checkMatches`. Expectations relate resolved symbolic inputs to results, for example: + +```kotlin +checkDiscoveredProperties( + method = "max2", + analysisResultsNumberMatcher = eq(count = 2), + { a: Number, b: Number, result: GoResult -> a.toLong() > b.toLong() && result.long == a.toLong() }, + { a: Number, b: Number, result: GoResult -> a.toLong() <= b.toLong() && result.long == b.toLong() }, +) +``` + +Every expected property must be discovered, and every collected execution must satisfy +at least one supplied expectation. `checkMatches` additionally requires a one-to-one +match between executions and expectations. The runner collects all terminated states, +with a 100-state limit and finite timeouts; these limits do not prove exhaustive analysis. + +`GoResult` distinguishes success from panic and exposes the panic payload. Mutation +checks can inspect argument snapshots before and after execution. Pointers and interfaces +retain structured values and interface dynamic types; these snapshots do not preserve +object identity or the complete alias graph. Function parameters remain mocked: the +`call` example checks that mock contract, rather than replaying an actual function body. + +The thematic `*RegressionTest` classes retain comparisons against native Go for 46 +zero-argument scalar/panic cases. `SymbolicBranchTest` and `SymbolicSliceAliasTest` also +check input-dependent properties, then replay five generated concrete inputs in a native +Go executable. Native scalar comparisons currently use textual representations; native +panic comparisons check occurrence, while symbolic sample expectations may also check +the payload. Native replay does not yet support arbitrary collection/struct inputs. + +The five slow examples remain manual and bounded: ```sh ./gradlew :usvm-go:manualTest --configure-on-demand ``` +The nonterminating `loopInfinite` example expects no completed executions within the +analysis budget. The remaining manual examples and `panicRecoverComplex` permit partial +instruction coverage but still enforce their semantic expectations. + +The stronger symbolic expectations expose unresolved input-model and semantic defects. +They are kept as failing tests, without disabled tests or expected-failure wrappers. +The current local run has **156 default tests: 102 passed, 54 failed**, plus +**5 manual tests: 3 passed, 2 failed**, with no skipped tests. Detekt on Go main/test +sources and the project-list check pass. Failures are grouped as follows: + +| Test package | Default failures | +| --- | ---: | +| `collections.slices` | 20 | +| `types` | 13 | +| `collections.maps` | 9 | +| `objects` | 4 | +| `strings` | 4 | +| `algorithms` | 2 | +| `arrays` | 1 | +| `pointers` | 1 | + +The manual failures are `canVisitAllRooms` and `mapLoopLen`. Counts are local-run observations, not a +count of independent bugs or an exhaustive list; budgeted symbolic exploration and +model materialization may affect which witnesses are collected. This draft integration +requires those failures to be resolved before merging. A concrete +native comparison is `nilMapLookup`: Go returns zero for a missing key in a nil map, +while USVM currently produces panic. This diagnostic permits partial instruction +coverage to expose the value/panic mismatch; it still requires one matching execution. + `generateGoImports` can export the import examples for investigation. The original import/standard-library exploratory factories depended on manually prepared dumps; they are not included in the default suite. diff --git a/usvm-go/build.gradle.kts b/usvm-go/build.gradle.kts index 5bff557089..ef97d8419e 100644 --- a/usvm-go/build.gradle.kts +++ b/usvm-go/build.gradle.kts @@ -72,4 +72,5 @@ tasks.named("manualTest") { tasks.withType().configureEach { ignoreFailures = false + setExcludes(listOf("**/resources/**", "**/build/**", "**/generated/**")) } diff --git a/usvm-go/src/main/go/regressions/semantics.go b/usvm-go/src/main/go/regressions/semantics.go index 33b5f5f8b2..5ce6ee9e59 100644 --- a/usvm-go/src/main/go/regressions/semantics.go +++ b/usvm-go/src/main/go/regressions/semantics.go @@ -280,3 +280,8 @@ func unsignedResultWidth() uint64 { value := uint64(0xffffffffffffffff) return value } + +func nilMapLookup() int { + var values map[int]int + return values[7] +} diff --git a/usvm-go/src/main/go/regressions/semantics_test.go b/usvm-go/src/main/go/regressions/semantics_test.go index e10b9d4cec..4615024dce 100644 --- a/usvm-go/src/main/go/regressions/semantics_test.go +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -9,6 +9,7 @@ import ( func TestNativeOracle(t *testing.T) { cases := map[string]func() any{ + "nilMapLookup": func() any { return nilMapLookup() }, "unsignedResultWidth": func() any { return unsignedResultWidth() }, "shiftByBitWidth": func() any { return shiftByBitWidth() }, "bitwiseComplement": func() any { return bitwiseComplement() }, 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 367927021a..481ec8dbcf 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -84,14 +84,19 @@ class GoTestInterpreter( inputScope.convertExpr(expr, type) } val inputModel = InputModel(inputValues) + val argumentsAfter = List(method.parameters.size) { index -> + val type = method.parameters[index].type as GoType + val original = model.read(URegisterStackLValue(typeToSort(type), index)) + outputScope.convertExpr(original, type) + } return if (state.isExceptional) { val panic = state.methodResult as GoMethodResult.Panic - UnsuccessfulExecutionResult(inputModel, outputScope.convertExpr(panic.value, panic.type)) + UnsuccessfulExecutionResult(inputModel, outputScope.convertExpr(panic.value, panic.type), argumentsAfter) } else { val result = state.methodResult as GoMethodResult.Success val expr = result.let { outputScope.convertExpr(it.value, it.type) } - val outputModel = OutputModel(expr) + val outputModel = OutputModel(expr, argumentsAfter) SuccessfulExecutionResult(inputModel, outputModel) } @@ -373,7 +378,7 @@ class GoTestInterpreter( val type = memory.typeStreamOf(iface).first() val index = 0 - return convertExpr(memory.readField(iface, index, typeToSort(type)), type) + return GoInterfaceValue(type, convertExpr(memory.readField(iface, index, typeToSort(type)), type)) } fun resolvePointer(pointer: UHeapRef, baseType: GoType): Any? = with(ctx) { @@ -392,7 +397,7 @@ class GoTestInterpreter( } else { memory.read(target) } - return "&" + convertExpr(expr, baseType) + return GoPointer(convertExpr(expr, baseType)) } fun resolveBoxed(value: UHeapRef, type: GoType): Any? = with(ctx) { @@ -443,6 +448,7 @@ class InputModel( class OutputModel( val returnExpr: Any?, + val argumentsAfter: List, ) { override fun toString(): String { return buildString { @@ -472,7 +478,8 @@ class SuccessfulExecutionResult( class UnsuccessfulExecutionResult( val inputModel: InputModel, - private val result: Any?, + val panicValue: Any?, + val argumentsAfter: List, ) : ProgramExecutionResult { override fun toString(): String { return buildString { @@ -481,7 +488,7 @@ class UnsuccessfulExecutionResult( appendLine("----------------------------------------------------------------") appendLine(inputModel.toString()) appendLine("----------------------------------------------------------------") - appendLine(result) + appendLine(panicValue) appendLine("================================================================") } } @@ -554,3 +561,11 @@ class RNG( /** A function supplied to a mocked call; it cannot currently be replayed as a concrete Go function. */ data class GoFunctionReference(val expression: String) + +data class GoPointer(val value: Any?) { + override fun toString(): String = "&$value" +} + +data class GoInterfaceValue(val type: GoType, val value: Any?) { + override fun toString(): String = value.toString() +} diff --git a/usvm-go/src/test/kotlin/org/usvm/GoSemanticRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/GoSemanticRegressionTest.kt deleted file mode 100644 index 3130378f10..0000000000 --- a/usvm-go/src/test/kotlin/org/usvm/GoSemanticRegressionTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -package org.usvm - -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.TestFactory -import org.usvm.interpreter.SuccessfulExecutionResult -import org.usvm.interpreter.UnsuccessfulExecutionResult -import org.usvm.model.Converter -import org.usvm.model.Parser -import java.io.File -import java.nio.file.Files -import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds - -class GoSemanticRegressionTest { - @TestFactory - fun replayGeneratedInputsWithNativeGo(): Collection = listOf( - "symbolicBranch" to 3, - "symbolicSliceAlias" to 2, - ).map { (name, count) -> DynamicTest.dynamicTest(name) { replayInputs(name, expectedExecutions = count) } } - - private fun replayInputs(methodName: String, expectedExecutions: Int) { - val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("regressions/usvm_regressions.json").path)) - val options = UMachineOptions(timeout = 5.seconds, solverTimeout = 2.seconds, typeOperationsTimeout = 2.seconds) - val customOptions = GoMachineOptions(failOnNotFullCoverage = true, uncoveredMethods = emptyList()) - val executions = GoMachine(GoProgram(listOf(pkg)), options, customOptions).use { machine -> - machine.analyzeAndResolve(pkg, methodName).map { assertIs(it) } - } - assertEquals(expectedExecutions, executions.size, message = "One witness per branch") - val inputs = executions.map { (it.inputModel.arguments.single() as Number).toLong() } - if (methodName == "symbolicBranch") { - assertTrue(inputs.any { it < 0 } && inputs.any { it == 0L } && inputs.any { it > 0 }) - } - - val replayFile = Files.createTempFile("usvm-go-replay-", ".json").toFile() - val logFile = Files.createTempFile("usvm-go-replay-", ".log").toFile() - try { - replayFile.writeText(inputs.joinToString(prefix = "[", postfix = "]")) - val builder = ProcessBuilder( - generatedGoFile("native-replay.test").path, - "-test.run=^TestReplaySymbolicInputs$" - ) - .redirectErrorStream(true).redirectOutput(logFile) - builder.environment()["USVM_GO_REPLAY_FILE"] = replayFile.path - builder.environment()["USVM_GO_REPLAY_METHOD"] = methodName - val process = builder.start() - try { - assertTrue(process.waitFor(5, TimeUnit.SECONDS), "Native replay timed out") - assertEquals(expected = 0, actual = process.exitValue(), message = logFile.readText()) - } finally { - process.destroyForcibly() - } - val nativeOutputs = Json.parseToJsonElement(replayFile.readText()).jsonArray.map { it.jsonPrimitive.int } - val symbolicOutputs = executions.map { (it.outputModel.returnExpr as Number).toInt() } - assertEquals(nativeOutputs, symbolicOutputs, "Replay of generated inputs") - } finally { - replayFile.delete() - logFile.delete() - } - } - - @TestFactory - fun compareWithNativeGo(): Collection { - val nativeResults = Json.parseToJsonElement( - File(generatedGoFile("native-oracle.json").path).readText() - ).jsonObject - val pkg = Converter.unpack( - Parser().deserialize(filename = generatedGoFile("regressions/usvm_regressions.json").path) - ) - val expectedNames = pkg.methods - .filter { it.parameters.isEmpty() && it.metName != "init" } - .map { it.metName }.toSet() - assertEquals(expectedNames, nativeResults.keys, "Every zero-argument fixture must have a native oracle") - val program = GoProgram(listOf(pkg)) - val options = UMachineOptions( - pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), - coverageZone = CoverageZone.TRANSITIVE, - exceptionsPropagation = true, - timeout = 5.seconds, - solverTimeout = 2.seconds, - typeOperationsTimeout = 2.seconds, - ) - - return nativeResults.map { (name, nativeResult) -> - DynamicTest.dynamicTest(name) { - val expected = nativeResult.jsonPrimitive.content - val customOptions = GoMachineOptions( - failOnNotFullCoverage = expected != "panic", - uncoveredMethods = emptyList(), - ) - - val actual = GoMachine(program, options, customOptions).use { machine -> - val executions = machine.analyzeAndResolve(pkg, name) - assertEquals(expected = 1, actual = executions.size, message = "Expected one complete execution") - val execution = executions.single() - if (expected == "panic") { - assertIs(execution) - "panic" - } else { - assertIs(execution).outputModel.returnExpr.toString() - } - } - - assertEquals(expected, actual, message = "Native Go result for $name") - } - } - } -} diff --git a/usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.kt b/usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.kt deleted file mode 100644 index 9a572a1ba1..0000000000 --- a/usvm-go/src/test/kotlin/org/usvm/GoUnsupportedTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.usvm - -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.usvm.model.Converter -import org.usvm.model.Parser -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds - -class GoUnsupportedTest { - @Test - fun goroutinesAreRejectedAtEntry() = assertUnsupported("unsupportedGoroutine", "unsupported instructions") - - @Test - fun goroutinesAreRejectedInsideCalls() = assertUnsupported("unsupportedCaller", "unsupported instructions") - - @Test - fun twoSymbolicStringLengthsAreRejected() = assertUnsupported("symbolicStringEquality", "two symbolic lengths") - - private fun assertUnsupported(method: String, message: String) { - val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("regressions/usvm_regressions.json").path)) - val options = UMachineOptions(timeout = 5.seconds, solverTimeout = 2.seconds, typeOperationsTimeout = 2.seconds) - val customOptions = GoMachineOptions(failOnNotFullCoverage = true, uncoveredMethods = emptyList()) - - GoMachine(GoProgram(listOf(pkg)), options, customOptions).use { machine -> - val error = assertThrows { machine.analyzeAndResolve(pkg, method) } - assertTrue(error.message.orEmpty().contains(message), error.message) - } - } -} diff --git a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt deleted file mode 100644 index 11199fe194..0000000000 --- a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.usvm - -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.Tag -import org.junit.jupiter.api.TestFactory -import org.usvm.model.Converter -import org.usvm.model.Parser -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds - -/** Coverage and execution smoke tests; scalar semantics are checked by GoSemanticRegressionTest. */ -class JacoDbTest { - @TestFactory - fun fastSamples(): Collection = samples(slow = false) - - @Tag("manual") - @TestFactory - fun slowSamples(): Collection = samples(slow = true) - - private fun samples(slow: Boolean): Collection { - val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("examples/usvm_examples.json").path)) - val program = GoProgram(listOf(pkg)) - val options = UMachineOptions( - pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), - coverageZone = CoverageZone.TRANSITIVE, - exceptionsPropagation = true, - timeout = (if (slow) 30 else 5).seconds, - solverTimeout = 2.seconds, - typeOperationsTimeout = 2.seconds, - ) - val customOptions = GoMachineOptions( - failOnNotFullCoverage = true, - uncoveredMethods = listOf("panicRecoverComplex"), - ) - return pkg.methods.filter { '$' !in it.metName && (it.metName in slowMethods) == slow }.map { method -> - DynamicTest.dynamicTest(method.metName) { - GoMachine(program, options, customOptions).use { machine -> - val results = machine.analyzeAndResolve(pkg, method.metName) - assertTrue(results.isNotEmpty(), "No complete execution for ${method.metName}") - } - } - } - } - - private val slowMethods = setOf("loopInfinite", "loopInner", "loopCollatz", "mapLoopLen", "canVisitAllRooms") -} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoExpectedValues.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoExpectedValues.kt new file mode 100644 index 0000000000..140dd1cb62 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoExpectedValues.kt @@ -0,0 +1,83 @@ +package org.usvm.samples + +import org.jacodb.go.api.NamedType +import org.usvm.interpreter.GoInterfaceValue + +fun GoMap.hasLongKey(key: Long): Boolean = orEmpty().keys.any { it.longValue() == key } +fun GoInterfaceValue.isNamed(name: String): Boolean = (type as? NamedType)?.name == name + +fun innerSum(bound: Long): Long { + var sum = 0L + for (first in 0..99) { + for (second in 0..99) { + if (first + second < bound) sum += first + second + } + } + return sum +} + +fun collatz(input: Long): Long { + if (input <= 0 || input >= 100) return 0L + + var value = input + var steps = 0 + while (value != 1L) { + value = if (value % 2 == 0L) value / 2 else value * 3 + 1 + steps++ + } + return if (steps == 17) 1L else 2L +} + +fun validTwoSum(values: List, target: Long, result: GoResult): Boolean { + if (result.isPanic) return false + + val pair = result.list + if (pair == null) { + val seen = hashSetOf() + for (value in values) { + if (target - value in seen) return false + seen += value + } + return true + } + + if (pair.size != 2) return false + val first = pair[0].longValue() + val second = pair[1].longValue() + if (first == second || first !in 0.., distance: Long, result: GoResult): Boolean { + val counts = hashMapOf() + for ((index, value) in values.withIndex()) { + counts[value] = counts.getOrDefault(value, defaultValue = 0) + 1 + if (counts.getValue(value) >= 2) return result.isSuccess && result.value == true + + val previous = index.toLong() - distance + if (previous >= 0) { + if (previous >= values.size) return result.isPanic + val previousValue = values[previous.toInt()] + counts[previousValue] = counts.getOrDefault(previousValue, defaultValue = 0) - 1 + } + } + return result.isSuccess && result.value == false +} + +fun matchesRooms(rooms: GoSlice, result: GoResult): Boolean { + val keys = rooms.orEmpty().map { (it as? List<*>).longValues() } + if (keys.size <= 1 || keys.any { it.isEmpty() || it.any { key -> key !in 0..() + val pending = ArrayDeque() + pending.addLast(0) + while (pending.isNotEmpty()) { + val room = pending.removeFirst() + if (visited.add(room)) { + keys[room].forEach { pending.addLast(it.toInt()) } + } + } + return result.isSuccess && result.value == (visited.size == keys.size) +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoMethodTestRunner.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoMethodTestRunner.kt new file mode 100644 index 0000000000..cd49d56b10 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoMethodTestRunner.kt @@ -0,0 +1,348 @@ +package org.usvm.samples + +import org.usvm.CoverageZone +import org.usvm.GoMachine +import org.usvm.GoMachineOptions +import org.usvm.GoPackage +import org.usvm.GoProgram +import org.usvm.PathSelectionStrategy +import org.usvm.StateCollectionStrategy +import org.usvm.UMachineOptions +import org.usvm.generatedGoFile +import org.usvm.interpreter.GoInterfaceValue +import org.usvm.interpreter.SuccessfulExecutionResult +import org.usvm.interpreter.UnsuccessfulExecutionResult +import org.usvm.model.Converter +import org.usvm.model.Parser +import org.usvm.test.util.TestRunner +import org.usvm.test.util.checkers.AnalysisResultsNumberMatcher +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf +import kotlin.time.Duration.Companion.seconds + +open class GoMethodTestRunner( + private val fixture: String = "examples", +) : TestRunner?, Unit>() { + protected val pkg: GoPackage by lazy { + Converter.unpack(Parser().deserialize(generatedGoFile("$fixture/usvm_$fixture.json").path)) + } + + override var options = UMachineOptions( + pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), + stateCollectionStrategy = StateCollectionStrategy.ALL, + coverageZone = CoverageZone.TRANSITIVE, + exceptionsPropagation = true, + stopOnCoverage = 0, + collectedStatesLimit = 100, + timeout = 5.seconds, + solverTimeout = 2.seconds, + typeOperationsTimeout = 2.seconds, + ) + + protected var machineOptions = GoMachineOptions(failOnNotFullCoverage = true, uncoveredMethods = emptyList()) + + protected fun checkParameterMutations(method: String, vararg expectations: (GoExecution) -> Boolean) { + require(expectations.isNotEmpty()) { "A mutation expectation is required for $method" } + val specification: (GoExecution) -> Boolean = { execution -> + expectations.any { runCatching { it(execution) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + analysisResultsMatchers = expectations, + invariants = arrayOf(specification), + extractValuesToCheck = { listOf(it) }, + expectedTypesForExtractedValues = arrayOf(GoExecution::class), + checkMode = CheckMode.MATCH_PROPERTIES, + coverageChecker = { true }, + ) + } + + protected fun nativeInt(value: Long): Long = if (pkg.intSize == Int.SIZE_BITS) value.toInt().toLong() else value + + protected fun checkNoResults(method: String) { + internalCheck( + target = method, + analysisResultsNumberMatcher = org.usvm.test.util.checkers.noResultsExpected, + analysisResultsMatchers = emptyArray(), + invariants = emptyArray(), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = emptyArray(), + checkMode = CheckMode.MATCH_PROPERTIES, + coverageChecker = { true }, + ) + } + + override val typeTransformer: (Any?) -> KClass<*>? = { it?.let { value -> value::class } } + override val checkType: (KClass<*>?, KClass<*>?) -> Boolean = { expected, actual -> + expected == null || actual == null || actual.isSubclassOf(expected) + } + override val coverageRunner: (List) -> Unit = { } + override val runner: (String, UMachineOptions) -> List = { method, options -> + GoMachine(GoProgram(listOf(pkg)), options, machineOptions).use { machine -> + machine.analyzeAndResolve(pkg, method).map { execution -> + when (execution) { + is SuccessfulExecutionResult -> GoExecution( + execution.inputModel.arguments, + execution.outputModel.argumentsAfter, + GoResult(value = execution.outputModel.returnExpr, isPanic = false), + ) + is UnsuccessfulExecutionResult -> GoExecution( + execution.inputModel.arguments, + execution.argumentsAfter, + GoResult(value = execution.panicValue, isPanic = true), + ) + } + } + } + } + + protected fun checkDiscoveredProperties( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (GoResult) -> Boolean = { result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(GoResult::class), + checkMode = CheckMode.MATCH_PROPERTIES, + coverageChecker = { true }, + ) + } + + protected inline fun checkDiscoveredProperties( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, GoResult) -> Boolean = { a0, result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(a0, result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, GoResult::class), + checkMode = CheckMode.MATCH_PROPERTIES, + coverageChecker = { true }, + ) + } + + protected inline fun checkDiscoveredProperties( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, A1, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, A1, GoResult) -> Boolean = { a0, a1, result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(a0, a1, result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, A1::class, GoResult::class), + checkMode = CheckMode.MATCH_PROPERTIES, + coverageChecker = { true }, + ) + } + + protected inline fun checkDiscoveredProperties( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, A1, A2, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, A1, A2, GoResult) -> Boolean = { a0, a1, a2, result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(a0, a1, a2, result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, A1::class, A2::class, GoResult::class), + checkMode = CheckMode.MATCH_PROPERTIES, + coverageChecker = { true }, + ) + } + + protected inline fun checkDiscoveredProperties( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, A1, A2, A3, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, A1, A2, A3, GoResult) -> Boolean = + { a0, a1, a2, a3, result -> + analysisResultsMatchers.any { matcher -> + runCatching { matcher(a0, a1, a2, a3, result) }.getOrDefault(false) + } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, A1::class, A2::class, A3::class, GoResult::class), + checkMode = CheckMode.MATCH_PROPERTIES, + coverageChecker = { true }, + ) + } + + protected fun checkMatches( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (GoResult) -> Boolean = { result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(GoResult::class), + checkMode = CheckMode.MATCH_EXECUTIONS, + coverageChecker = { true }, + ) + } + + protected inline fun checkMatches( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, GoResult) -> Boolean = { a0, result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(a0, result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, GoResult::class), + checkMode = CheckMode.MATCH_EXECUTIONS, + coverageChecker = { true }, + ) + } + + protected inline fun checkMatches( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, A1, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, A1, GoResult) -> Boolean = { a0, a1, result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(a0, a1, result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, A1::class, GoResult::class), + checkMode = CheckMode.MATCH_EXECUTIONS, + coverageChecker = { true }, + ) + } + + protected inline fun checkMatches( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, A1, A2, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, A1, A2, GoResult) -> Boolean = { a0, a1, a2, result -> + analysisResultsMatchers.any { matcher -> runCatching { matcher(a0, a1, a2, result) }.getOrDefault(false) } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, A1::class, A2::class, GoResult::class), + checkMode = CheckMode.MATCH_EXECUTIONS, + coverageChecker = { true }, + ) + } + + protected inline fun checkMatches( + method: String, + analysisResultsNumberMatcher: AnalysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + vararg analysisResultsMatchers: (A0, A1, A2, A3, GoResult) -> Boolean, + ) { + require(analysisResultsMatchers.isNotEmpty()) { "A semantic expectation is required for $method" } + val specification: (A0, A1, A2, A3, GoResult) -> Boolean = + { a0, a1, a2, a3, result -> + analysisResultsMatchers.any { matcher -> + runCatching { matcher(a0, a1, a2, a3, result) }.getOrDefault(false) + } + } + + internalCheck( + target = method, + analysisResultsNumberMatcher = analysisResultsNumberMatcher, + analysisResultsMatchers = analysisResultsMatchers, + invariants = arrayOf(specification), + extractValuesToCheck = { it.arguments + it.result }, + expectedTypesForExtractedValues = arrayOf(A0::class, A1::class, A2::class, A3::class, GoResult::class), + checkMode = CheckMode.MATCH_EXECUTIONS, + coverageChecker = { true }, + ) + } +} + +data class GoExecution(val arguments: List, val argumentsAfter: List, val result: GoResult) + +data class GoResult(val value: Any?, val isPanic: Boolean) { + val isSuccess: Boolean get() = !isPanic + val panicValue: Any? get() = if (isPanic) (value as? GoInterfaceValue)?.value ?: value else null + val long: Long? get() = if (isSuccess) (value as? Number)?.toLong() else null + val list: List<*>? get() = if (isSuccess) value as? List<*> else null + val map: Map<*, *>? get() = if (isSuccess) value as? Map<*, *> else null +} + +typealias GoSlice = List<*>? +typealias GoMap = Map<*, *>? +typealias GoStruct = Map<*, *>? + +fun Any?.longValue(): Long = (this as Number).toLong() +fun GoSlice.longValues(): List = orEmpty().map { it.longValue() } +fun GoMap.longEntry(key: Long): Long = orEmpty().entries.firstOrNull { + it.key.longValue() == key +}?.value?.longValue() ?: 0 +fun GoStruct.field(index: Int): Any? = this?.get("field$index") diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt new file mode 100644 index 0000000000..18dc398300 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt @@ -0,0 +1,65 @@ +package org.usvm.samples + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import org.usvm.generatedGoFile +import org.usvm.test.util.checkers.eq +import java.nio.file.Files +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +open class GoNativeTestRunner : GoMethodTestRunner(fixture = "regressions") { + protected fun checkNative(method: String) { + val expected = Json.parseToJsonElement(generatedGoFile("native-oracle.json").readText()) + .jsonObject.getValue(method).jsonPrimitive.content + if (expected == "panic") machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkMatches( + method = method, + analysisResultsNumberMatcher = eq(count = 1), + { result: GoResult -> + if (expected == "panic") result.isPanic else result.isSuccess && result.value.toString() == expected + }, + ) + } + + protected fun replayInputs(methodName: String, expectedExecutions: Int) { + val executions = runner(methodName, options) + assertTrue(executions.all { it.result.isSuccess }, "Native replay expects successful executions") + assertEquals(expectedExecutions, executions.size, message = "One witness per branch") + val inputs = executions.map { (it.arguments.single() as Number).toLong() } + if (methodName == "symbolicBranch") { + assertTrue(inputs.any { it < 0 } && inputs.any { it == 0L } && inputs.any { it > 0 }) + } + + val replayFile = Files.createTempFile("usvm-go-replay-", ".json").toFile() + val logFile = Files.createTempFile("usvm-go-replay-", ".log").toFile() + try { + replayFile.writeText(inputs.joinToString(prefix = "[", postfix = "]")) + val builder = ProcessBuilder( + generatedGoFile("native-replay.test").path, + "-test.run=^TestReplaySymbolicInputs$" + ) + .redirectErrorStream(true).redirectOutput(logFile) + builder.environment()["USVM_GO_REPLAY_FILE"] = replayFile.path + builder.environment()["USVM_GO_REPLAY_METHOD"] = methodName + val process = builder.start() + try { + assertTrue(process.waitFor(5, TimeUnit.SECONDS), "Native replay timed out") + assertEquals(expected = 0, actual = process.exitValue(), message = logFile.readText()) + } finally { + process.destroyForcibly() + } + val nativeOutputs = Json.parseToJsonElement(replayFile.readText()).jsonArray.map { it.jsonPrimitive.long } + val symbolicOutputs = executions.map { (it.result.value as Number).toLong() } + assertEquals(nativeOutputs, symbolicOutputs, "Replay of generated inputs") + } finally { + replayFile.delete() + logFile.delete() + } + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoSample.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoSample.kt new file mode 100644 index 0000000000..965e55478c --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoSample.kt @@ -0,0 +1,5 @@ +package org.usvm.samples + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class GoSample(val method: String, val fixture: String = "examples") diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt new file mode 100644 index 0000000000..2c6e3d8cda --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt @@ -0,0 +1,72 @@ +package org.usvm.samples + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import org.junit.jupiter.api.Test +import org.usvm.generatedGoFile +import org.usvm.model.Converter +import org.usvm.model.Parser +import kotlin.test.assertEquals + +class SampleCoverageTest { + @Test + fun everyExportedSampleHasAnExplicitTest() { + val samples = sampleClasses.flatMap { clazz -> + clazz.declaredMethods.mapNotNull { it.getAnnotation(GoSample::class.java) } + } + for (fixture in listOf("examples", "regressions")) { + val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("$fixture/usvm_$fixture.json").path)) + val expected = pkg.methods.filter { '$' !in it.metName }.map { it.metName }.toSet() + val registered = samples.filter { it.fixture == fixture }.map { it.method } + + assertEquals(expected, registered.toSet(), "Every $fixture method must have a semantic test") + assertEquals(registered.toSet().size, registered.size, "Each $fixture method must be registered once") + } + } + + @Test + fun everyConstantRegressionHasANativeOracle() { + val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("regressions/usvm_regressions.json").path)) + val expected = pkg.methods.filter { it.parameters.isEmpty() && it.metName != "init" }.map { it.metName }.toSet() + val oracle = Json.parseToJsonElement(generatedGoFile("native-oracle.json").readText()).jsonObject + + assertEquals(expected, oracle.keys) + } + + private val sampleClasses = listOf( + org.usvm.samples.algorithms.AlgorithmsTest::class.java, + org.usvm.samples.algorithms.SlowAlgorithmsTest::class.java, + org.usvm.samples.arithmetic.ArithmeticRegressionTest::class.java, + org.usvm.samples.arrays.ArrayRegressionTest::class.java, + org.usvm.samples.arrays.ArraysTest::class.java, + org.usvm.samples.calls.CallsTest::class.java, + org.usvm.samples.collections.maps.MapAllocationTest::class.java, + org.usvm.samples.collections.maps.MapIterationTest::class.java, + org.usvm.samples.collections.maps.MapLookupTest::class.java, + org.usvm.samples.collections.maps.MapMutationTest::class.java, + org.usvm.samples.collections.maps.MapRegressionTest::class.java, + org.usvm.samples.collections.maps.SlowMapIterationTest::class.java, + org.usvm.samples.collections.slices.SliceAlgorithmsTest::class.java, + org.usvm.samples.collections.slices.SliceAllocationTest::class.java, + org.usvm.samples.collections.slices.SliceMutationTest::class.java, + org.usvm.samples.collections.slices.SliceRegressionTest::class.java, + org.usvm.samples.collections.slices.SliceViewsTest::class.java, + org.usvm.samples.collections.slices.SymbolicSliceAliasTest::class.java, + org.usvm.samples.controlflow.ConditionsTest::class.java, + org.usvm.samples.controlflow.LoopsTest::class.java, + org.usvm.samples.controlflow.SlowLoopsTest::class.java, + org.usvm.samples.controlflow.SymbolicBranchTest::class.java, + org.usvm.samples.exceptions.DeferTest::class.java, + org.usvm.samples.globals.GlobalsTest::class.java, + org.usvm.samples.objects.ObjectsTest::class.java, + org.usvm.samples.pointers.PointersTest::class.java, + org.usvm.samples.strings.NamedStringsTest::class.java, + org.usvm.samples.strings.StringConstraintsTest::class.java, + org.usvm.samples.strings.StringRegressionTest::class.java, + org.usvm.samples.strings.StringsTest::class.java, + org.usvm.samples.types.InterfacesTest::class.java, + org.usvm.samples.types.NamedTypesTest::class.java, + org.usvm.samples.types.StructsTest::class.java, + org.usvm.samples.unsupported.GoUnsupportedTest::class.java, + ) +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/algorithms/AlgorithmsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/algorithms/AlgorithmsTest.kt new file mode 100644 index 0000000000..1da1955ddd --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/algorithms/AlgorithmsTest.kt @@ -0,0 +1,33 @@ +package org.usvm.samples.algorithms + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoSlice +import org.usvm.samples.longValues +import org.usvm.samples.matchesNearbyDuplicate +import org.usvm.samples.validTwoSum +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class AlgorithmsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "twoSum") + fun twoSum() { + checkDiscoveredProperties( + method = "twoSum", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { nums: GoSlice, target: Number, r: GoResult -> validTwoSum(nums.longValues(), target.toLong(), r) }, + ) + } + + @Test + @GoSample(method = "containsNearbyDuplicate") + fun containsNearbyDuplicate() { + checkDiscoveredProperties( + method = "containsNearbyDuplicate", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { nums: GoSlice, k: Number, r: GoResult -> matchesNearbyDuplicate(nums.longValues(), k.toLong(), r) }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/algorithms/SlowAlgorithmsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/algorithms/SlowAlgorithmsTest.kt new file mode 100644 index 0000000000..913fc5d085 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/algorithms/SlowAlgorithmsTest.kt @@ -0,0 +1,25 @@ +package org.usvm.samples.algorithms + +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoSlice +import org.usvm.samples.matchesRooms +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class SlowAlgorithmsTest : GoMethodTestRunner() { + @Tag(value = "manual") + @Test + @GoSample(method = "canVisitAllRooms") + fun canVisitAllRooms() { + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkDiscoveredProperties( + method = "canVisitAllRooms", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { rooms: GoSlice, r: GoResult -> matchesRooms(rooms, r) }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt new file mode 100644 index 0000000000..cccb0f0b1f --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt @@ -0,0 +1,63 @@ +package org.usvm.samples.arithmetic + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class ArithmeticRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "bitwiseAndNot", fixture = "regressions") + fun bitwiseAndNot() = checkNative(method = "bitwiseAndNot") + + @Test + @GoSample(method = "bitwiseAndOr", fixture = "regressions") + fun bitwiseAndOr() = checkNative(method = "bitwiseAndOr") + + @Test + @GoSample(method = "bitwiseComplement", fixture = "regressions") + fun bitwiseComplement() = checkNative(method = "bitwiseComplement") + + @Test + @GoSample(method = "divideByZero", fixture = "regressions") + fun divideByZero() = checkNative(method = "divideByZero") + + @Test + @GoSample(method = "nativeIntOverflow", fixture = "regressions") + fun nativeIntOverflow() = checkNative(method = "nativeIntOverflow") + + @Test + @GoSample(method = "nativeIntWidth", fixture = "regressions") + fun nativeIntWidth() = checkNative(method = "nativeIntWidth") + + @Test + @GoSample(method = "negativeShift", fixture = "regressions") + fun negativeShift() = checkNative(method = "negativeShift") + + @Test + @GoSample(method = "oversizedShiftCount", fixture = "regressions") + fun oversizedShiftCount() = checkNative(method = "oversizedShiftCount") + + @Test + @GoSample(method = "remainderByZero", fixture = "regressions") + fun remainderByZero() = checkNative(method = "remainderByZero") + + @Test + @GoSample(method = "shiftByBitWidth", fixture = "regressions") + fun shiftByBitWidth() = checkNative(method = "shiftByBitWidth") + + @Test + @GoSample(method = "signedRightShift", fixture = "regressions") + fun signedRightShift() = checkNative(method = "signedRightShift") + + @Test + @GoSample(method = "unsignedResultWidth", fixture = "regressions") + fun unsignedResultWidth() = checkNative(method = "unsignedResultWidth") + + @Test + @GoSample(method = "unsignedRightShift", fixture = "regressions") + fun unsignedRightShift() = checkNative(method = "unsignedRightShift") + + @Test + @GoSample(method = "unsignedWidening", fixture = "regressions") + fun unsignedWidening() = checkNative(method = "unsignedWidening") +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArrayRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArrayRegressionTest.kt new file mode 100644 index 0000000000..2af20287be --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArrayRegressionTest.kt @@ -0,0 +1,11 @@ +package org.usvm.samples.arrays + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class ArrayRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "narrowIndex", fixture = "regressions") + fun narrowIndex() = checkNative(method = "narrowIndex") +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArraysTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArraysTest.kt new file mode 100644 index 0000000000..cea61dab8d --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/arrays/ArraysTest.kt @@ -0,0 +1,47 @@ +package org.usvm.samples.arrays + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoSlice +import org.usvm.samples.longValue +import org.usvm.samples.longValues +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class ArraysTest : GoMethodTestRunner() { + @Test + @GoSample(method = "arrayIndex") + fun arrayIndex() { + checkDiscoveredProperties( + method = "arrayIndex", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: GoSlice, i: Number, r: GoResult -> i.toLong() !in 0..2 && r.isPanic }, + { a: GoSlice, i: Number, r: GoResult -> + i.toLong() in 0..2 && + r.long == a?.get(i.toInt()).longValue() + }, + ) + } + + @Test + @GoSample(method = "arrayIndexMake") + fun arrayIndexMake() { + checkDiscoveredProperties( + method = "arrayIndexMake", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, r: GoResult -> i.toLong() !in 0..2 && r.isPanic }, + { i: Number, r: GoResult -> i.toLong() in 0..2 && r.long == i.toLong() + 1 }, + ) + } + + @Test + @GoSample(method = "arraySlice") + fun arraySlice() { + checkDiscoveredProperties( + method = "arraySlice", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.list?.longValues() == listOf(2L) }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/calls/CallsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/calls/CallsTest.kt new file mode 100644 index 0000000000..495538d7f5 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/calls/CallsTest.kt @@ -0,0 +1,44 @@ +package org.usvm.samples.calls + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoFunctionReference +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.longValues +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class CallsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "beforeAndAfter") + fun beforeAndAfter() { + checkDiscoveredProperties( + method = "beforeAndAfter", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, r: GoResult -> + r.list?.longValues() == listOf(nativeInt(a.toLong() - 1), nativeInt(a.toLong() + 1)) + }, + ) + } + + @Test + @GoSample(method = "sumBeforeAndAfter") + fun sumBeforeAndAfter() { + checkDiscoveredProperties( + method = "sumBeforeAndAfter", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, r: GoResult -> r.long == nativeInt(a.toLong() * 2) }, + ) + } + + /** Function inputs are mocked; this checks the callback mock contract. */ + @Test + @GoSample(method = "call") + fun call() { + checkDiscoveredProperties( + method = "call", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { f: GoFunctionReference?, r: GoResult -> f != null && r.isSuccess && r.value is Number }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapAllocationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapAllocationTest.kt new file mode 100644 index 0000000000..1ca4e52eb2 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapAllocationTest.kt @@ -0,0 +1,35 @@ +package org.usvm.samples.collections.maps + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.longEntry +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class MapAllocationTest : GoMethodTestRunner() { + @Test + @GoSample(method = "mapAlloc") + fun mapAlloc() { + checkDiscoveredProperties( + method = "mapAlloc", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: Number, r: GoResult -> + r.map?.size == 3 && + r.map.longEntry(key = 3) == 111L && + r.map.longEntry(key = -226) == 13L && + r.map.longEntry(key = 0) == -1L + }, + ) + } + + @Test + @GoSample(method = "mapCustomAlloc") + fun mapCustomAlloc() { + checkDiscoveredProperties( + method = "mapCustomAlloc", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.map?.size == 1 && r.map.longEntry(key = 2) == 3L }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapIterationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapIterationTest.kt new file mode 100644 index 0000000000..cd71eda4f5 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapIterationTest.kt @@ -0,0 +1,25 @@ +package org.usvm.samples.collections.maps + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMap +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.longValue +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class MapIterationTest : GoMethodTestRunner() { + @Test + @GoSample(method = "mapLoop") + fun mapLoop() { + checkDiscoveredProperties( + method = "mapLoop", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, n: Number, r: GoResult -> + r.long == m.orEmpty().filterKeys { it.longValue() > n.toLong() }.values.fold(0L) { maximum, v -> + maxOf(maximum, v.longValue()) + } + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapLookupTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapLookupTest.kt new file mode 100644 index 0000000000..083ff64cdd --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapLookupTest.kt @@ -0,0 +1,96 @@ +package org.usvm.samples.collections.maps + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMap +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.hasLongKey +import org.usvm.samples.longEntry +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class MapLookupTest : GoMethodTestRunner() { + @Test + @GoSample(method = "mapLookup") + fun mapLookup() { + checkDiscoveredProperties( + method = "mapLookup", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, key: Number, r: GoResult -> m.longEntry(key.toLong()) == 123L && r.long == -1L }, + { m: GoMap, key: Number, r: GoResult -> + m.longEntry(key.toLong()) != 123L && + r.long == m.longEntry(key.toLong()) + }, + ) + } + + @Test + @GoSample(method = "mapCustomLookup") + fun mapCustomLookup() { + checkDiscoveredProperties( + method = "mapCustomLookup", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, key: Number, r: GoResult -> m.longEntry(key.toLong()) == 123L && r.long == -1L }, + { m: GoMap, key: Number, r: GoResult -> + m.longEntry(key.toLong()) != 123L && + r.long == m.longEntry(key.toLong()) + }, + ) + } + + @Test + @GoSample(method = "mapLookupComma") + fun mapLookupComma() { + checkDiscoveredProperties( + method = "mapLookupComma", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, key: Number, r: GoResult -> m.longEntry(key.toLong()) == 123L && r.long == -1L }, + { m: GoMap, key: Number, r: GoResult -> + m.longEntry(key.toLong()) != 123L && + r.long == m.longEntry(key.toLong()) + }, + ) + } + + @Test + @GoSample(method = "mapCustomLookupComma") + fun mapCustomLookupComma() { + checkDiscoveredProperties( + method = "mapCustomLookupComma", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, key: Number, r: GoResult -> m.longEntry(key.toLong()) == 123L && r.long == -1L }, + { m: GoMap, key: Number, r: GoResult -> + m.longEntry(key.toLong()) != 123L && + r.long == m.longEntry(key.toLong()) + }, + ) + } + + @Test + @GoSample(method = "mapLookupCommaReturn") + fun mapLookupCommaReturn() { + checkDiscoveredProperties( + method = "mapLookupCommaReturn", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, key: Number, r: GoResult -> !m.hasLongKey(key.toLong()) && r.list == listOf(-1L, false) }, + { m: GoMap, key: Number, r: GoResult -> + m.hasLongKey(key.toLong()) && + r.list == listOf(m.longEntry(key.toLong()), true) + }, + ) + } + + @Test + @GoSample(method = "mapCustomLookupCommaReturn") + fun mapCustomLookupCommaReturn() { + checkDiscoveredProperties( + method = "mapCustomLookupCommaReturn", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, key: Number, r: GoResult -> !m.hasLongKey(key.toLong()) && r.list == listOf(-1L, false) }, + { m: GoMap, key: Number, r: GoResult -> + m.hasLongKey(key.toLong()) && + r.list == listOf(m.longEntry(key.toLong()), true) + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapMutationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapMutationTest.kt new file mode 100644 index 0000000000..14874622d6 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapMutationTest.kt @@ -0,0 +1,82 @@ +package org.usvm.samples.collections.maps + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMap +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.hasLongKey +import org.usvm.samples.longEntry +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class MapMutationTest : GoMethodTestRunner() { + @Test + @GoSample(method = "mapUpdate") + fun mapUpdate() { + checkDiscoveredProperties( + method = "mapUpdate", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, _: Number, _: Number, r: GoResult -> m == null && r.isPanic }, + { m: GoMap, key: Number, value: Number, r: GoResult -> + m != null && + r.long == maxOf(m.longEntry(key.toLong()), value.toLong()) + }, + ) + } + + @Test + @GoSample(method = "mapCustomUpdate") + fun mapCustomUpdate() { + checkDiscoveredProperties( + method = "mapCustomUpdate", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, _: Number, _: Number, r: GoResult -> m == null && r.isPanic }, + { m: GoMap, key: Number, value: Number, r: GoResult -> + m != null && + r.long == maxOf(m.longEntry(key.toLong()), value.toLong()) + }, + ) + } + + @Test + @GoSample(method = "mapDeleteSimple") + fun mapDeleteSimple() { + checkDiscoveredProperties( + method = "mapDeleteSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, _: Number, r: GoResult -> m.orEmpty().size < 5 && r.isPanic && r.panicValue == "too smol map" }, + { m: GoMap, key: Number, r: GoResult -> + m.orEmpty().size >= 5 && + !m.hasLongKey(key.toLong()) && + r.isPanic && + r.panicValue == "not found" + }, + { m: GoMap, key: Number, r: GoResult -> + m.orEmpty().size >= 5 && + m.hasLongKey(key.toLong()) && + r.long == m.orEmpty().size.toLong() - 1 + }, + ) + } + + @Test + @GoSample(method = "mapCustomDeleteSimple") + fun mapCustomDeleteSimple() { + checkDiscoveredProperties( + method = "mapCustomDeleteSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, _: Number, r: GoResult -> m.orEmpty().size < 5 && r.isPanic && r.panicValue == "too smol map" }, + { m: GoMap, key: Number, r: GoResult -> + m.orEmpty().size >= 5 && + !m.hasLongKey(key.toLong()) && + r.isPanic && + r.panicValue == "not found" + }, + { m: GoMap, key: Number, r: GoResult -> + m.orEmpty().size >= 5 && + m.hasLongKey(key.toLong()) && + r.long == m.orEmpty().size.toLong() - 1 + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt new file mode 100644 index 0000000000..43ac5c2b21 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt @@ -0,0 +1,32 @@ +package org.usvm.samples.collections.maps + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class MapRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "mapDeleteLength", fixture = "regressions") + fun mapDeleteLength() = checkNative(method = "mapDeleteLength") + + @Test + @GoSample(method = "mapHintLength", fixture = "regressions") + fun mapHintLength() = checkNative(method = "mapHintLength") + + @Test + @GoSample(method = "mapInsertLength", fixture = "regressions") + fun mapInsertLength() = checkNative(method = "mapInsertLength") + + @Test + @GoSample(method = "mapOverwriteLength", fixture = "regressions") + fun mapOverwriteLength() = checkNative(method = "mapOverwriteLength") + + @Test + @GoSample(method = "nilMapLookup", fixture = "regressions") + fun nilMapLookup() { + // Report the return/panic mismatch even when the frontend cannot cover the return instruction. + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkNative(method = "nilMapLookup") + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt new file mode 100644 index 0000000000..2e00d4b36c --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt @@ -0,0 +1,33 @@ +package org.usvm.samples.collections.maps + +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMap +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.longValue +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class SlowMapIterationTest : GoMethodTestRunner() { + @Tag(value = "manual") + @Test + @GoSample(method = "mapLoopLen") + fun mapLoopLen() { + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkDiscoveredProperties( + method = "mapLoopLen", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, r: GoResult -> + val values = m.orEmpty().values.map { it.longValue() } + val expected = if (values.size < 4) { + -1L + } else { + nativeInt(maxOf(0L, values.max()) - minOf(0L, values.min())) + } + r.long == expected + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAlgorithmsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAlgorithmsTest.kt new file mode 100644 index 0000000000..211186bd77 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAlgorithmsTest.kt @@ -0,0 +1,93 @@ +package org.usvm.samples.collections.slices + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoSlice +import org.usvm.samples.longValue +import org.usvm.samples.longValues +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class SliceAlgorithmsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "sliceFirst") + fun sliceFirst() { + checkDiscoveredProperties( + method = "sliceFirst", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().size !in 2..20 && r.long == -1L }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size in 2..20 && + a?.get(0).longValue() == 1L && + a?.get(1).longValue() == 2L && + r.long == 1L + }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size in 2..20 && + (a?.get(0).longValue() != 1L || a?.get(1).longValue() != 2L) && + r.long == 0L + }, + ) + } + + @Test + @GoSample(method = "sliceSum") + fun sliceSum() { + checkDiscoveredProperties( + method = "sliceSum", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().size < 5 && r.long == -228L }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size >= 5 && + r.long == nativeInt(a.longValues().sumOf { if (it < 0) -it else it }) + }, + ) + } + + @Test + @GoSample(method = "sliceCompare") + fun sliceCompare() { + checkDiscoveredProperties( + method = "sliceCompare", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().size < 2 && r.isPanic }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size >= 2 && + r.long == minOf(a?.get(0).longValue(), a?.get(1).longValue()) + }, + ) + } + + @Test + @GoSample(method = "sliceCompareFuncVar") + fun sliceCompareFuncVar() { + checkDiscoveredProperties( + method = "sliceCompareFuncVar", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().size < 2 && r.isPanic }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size >= 2 && + r.long == if (minOf(a?.get(0).longValue(), a?.get(1).longValue()) == 5L) 3L else 1L + }, + ) + } + + @Test + @GoSample(method = "sliceSumMatrix") + fun sliceSumMatrix() { + checkDiscoveredProperties( + method = "sliceSumMatrix", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> + (a.orEmpty().size < 3 || (a?.get(0) as? List<*>).orEmpty().size < 3) && + r.isPanic + }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size >= 3 && + (a?.get(0) as? List<*>).orEmpty().size >= 3 && + r.long == nativeInt(a.orEmpty().sumOf { (it as? List<*>).longValues().sum() }) + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAllocationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAllocationTest.kt new file mode 100644 index 0000000000..565e069c43 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceAllocationTest.kt @@ -0,0 +1,79 @@ +package org.usvm.samples.collections.slices + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoSlice +import org.usvm.samples.longValues +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class SliceAllocationTest : GoMethodTestRunner() { + @Test + @GoSample(method = "sliceAlloc") + fun sliceAlloc() { + checkDiscoveredProperties( + method = "sliceAlloc", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { size: Number, r: GoResult -> size.toLong() < 0 && r.isPanic }, + { size: Number, r: GoResult -> + size.toLong() >= 0 && r.list?.longValues() == List(size.toInt()) { index -> + if (size.toLong() in 5..10 && index == 3) 111L else 0L + } + }, + ) + } + + @Test + @GoSample(method = "sliceCustomAppend") + fun sliceCustomAppend() { + checkDiscoveredProperties( + method = "sliceCustomAppend", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.list?.longValues() == listOf(1L, 2L, 3L, 4L, 5L, 123L) }, + ) + } + + @Test + @GoSample(method = "sliceAppend") + fun sliceAppend() { + checkDiscoveredProperties( + method = "sliceAppend", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> r.list?.longValues() == a.longValues() + listOf(3L, 1L, 2L, 4L) }, + ) + } + + @Test + @GoSample(method = "sliceAppendSimple") + fun sliceAppendSimple() { + checkDiscoveredProperties( + method = "sliceAppendSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().isEmpty() && r.list?.longValues() == listOf(1L, 2L) }, + { a: GoSlice, r: GoResult -> + a.orEmpty().isNotEmpty() && + r.list?.longValues() == a.longValues() + listOf(5L, 6L) + }, + ) + } + + @Test + @GoSample(method = "sliceAppendTwo") + fun sliceAppendTwo() { + checkDiscoveredProperties( + method = "sliceAppendTwo", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, b: GoSlice, r: GoResult -> + (a.orEmpty().size !in 5..10 || b.orEmpty().size !in 5..10) && + r.isSuccess && + r.value == null + }, + { a: GoSlice, b: GoSlice, r: GoResult -> + a.orEmpty().size in 5..10 && + b.orEmpty().size in 5..10 && + r.list?.longValues() == a.longValues() + b.longValues() + 4L + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceMutationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceMutationTest.kt new file mode 100644 index 0000000000..9944b5798e --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceMutationTest.kt @@ -0,0 +1,70 @@ +package org.usvm.samples.collections.slices + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoSlice +import org.usvm.samples.longValues +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class SliceMutationTest : GoMethodTestRunner() { + @Test + @GoSample(method = "sliceOverwrite") + fun sliceOverwrite() { + checkDiscoveredProperties( + method = "sliceOverwrite", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().isEmpty() && r.long == -1L }, + { a: GoSlice, r: GoResult -> a.orEmpty().isNotEmpty() && r.long == 152L }, + ) + } + + @Test + @GoSample(method = "sliceCustomOverwrite") + fun sliceCustomOverwrite() { + checkDiscoveredProperties( + method = "sliceCustomOverwrite", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().isEmpty() && r.long == -1L }, + { a: GoSlice, r: GoResult -> a.orEmpty().isNotEmpty() && r.long == 152L }, + ) + } + + @Test + @GoSample(method = "sliceSimple") + fun sliceSimple() { + checkDiscoveredProperties( + method = "sliceSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().isEmpty() && r.isPanic }, + { a: GoSlice, r: GoResult -> a.orEmpty().isNotEmpty() && r.long == 5L }, + ) + } + + @Test + @GoSample(method = "sliceCopySimple") + fun sliceCopySimple() { + checkDiscoveredProperties( + method = "sliceCopySimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> + r.isSuccess && + r.list?.longValues() == a?.longValues()?.let { if (it.isEmpty()) it else listOf(5L) + it.drop(1) } + }, + ) + } + + @Test + @GoSample(method = "sliceCustomCopySimple") + fun sliceCustomCopySimple() { + checkDiscoveredProperties( + method = "sliceCustomCopySimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> + r.isSuccess && + r.list?.longValues() == a?.longValues()?.let { if (it.isEmpty()) it else listOf(5L) + it.drop(1) } + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceRegressionTest.kt new file mode 100644 index 0000000000..cfa999a600 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceRegressionTest.kt @@ -0,0 +1,71 @@ +package org.usvm.samples.collections.slices + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class SliceRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "negativeIndex", fixture = "regressions") + fun negativeIndex() = checkNative(method = "negativeIndex") + + @Test + @GoSample(method = "negativeSliceHigh", fixture = "regressions") + fun negativeSliceHigh() = checkNative(method = "negativeSliceHigh") + + @Test + @GoSample(method = "nilSliceAppend", fixture = "regressions") + fun nilSliceAppend() = checkNative(method = "nilSliceAppend") + + @Test + @GoSample(method = "nilSliceLength", fixture = "regressions") + fun nilSliceLength() = checkNative(method = "nilSliceLength") + + @Test + @GoSample(method = "oversizedIndex", fixture = "regressions") + fun oversizedIndex() = checkNative(method = "oversizedIndex") + + @Test + @GoSample(method = "sliceAlias", fixture = "regressions") + fun sliceAlias() = checkNative(method = "sliceAlias") + + @Test + @GoSample(method = "sliceAppendAllocate", fixture = "regressions") + fun sliceAppendAllocate() = checkNative(method = "sliceAppendAllocate") + + @Test + @GoSample(method = "sliceAppendOffset", fixture = "regressions") + fun sliceAppendOffset() = checkNative(method = "sliceAppendOffset") + + @Test + @GoSample(method = "sliceAppendReuse", fixture = "regressions") + fun sliceAppendReuse() = checkNative(method = "sliceAppendReuse") + + @Test + @GoSample(method = "sliceArrayPointerAlias", fixture = "regressions") + fun sliceArrayPointerAlias() = checkNative(method = "sliceArrayPointerAlias") + + @Test + @GoSample(method = "sliceCapacity", fixture = "regressions") + fun sliceCapacity() = checkNative(method = "sliceCapacity") + + @Test + @GoSample(method = "sliceCopyOffset", fixture = "regressions") + fun sliceCopyOffset() = checkNative(method = "sliceCopyOffset") + + @Test + @GoSample(method = "sliceCopyString", fixture = "regressions") + fun sliceCopyString() = checkNative(method = "sliceCopyString") + + @Test + @GoSample(method = "sliceFullCapacity", fixture = "regressions") + fun sliceFullCapacity() = checkNative(method = "sliceFullCapacity") + + @Test + @GoSample(method = "sliceOffsetAlias", fixture = "regressions") + fun sliceOffsetAlias() = checkNative(method = "sliceOffsetAlias") + + @Test + @GoSample(method = "unsignedSliceLength", fixture = "regressions") + fun unsignedSliceLength() = checkNative(method = "unsignedSliceLength") +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceViewsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceViewsTest.kt new file mode 100644 index 0000000000..4258ace074 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SliceViewsTest.kt @@ -0,0 +1,138 @@ +package org.usvm.samples.collections.slices + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoPointer +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoSlice +import org.usvm.samples.longValues +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class SliceViewsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "sliceCustomSlice") + fun sliceCustomSlice() { + checkDiscoveredProperties( + method = "sliceCustomSlice", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.list?.longValues() == listOf(2L, 3L) }, + ) + } + + @Test + @GoSample(method = "sliceSliceFull") + fun sliceSliceFull() { + checkDiscoveredProperties( + method = "sliceSliceFull", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().size !in 5..10 && r.isSuccess && r.value == null }, + { a: GoSlice, r: GoResult -> a.orEmpty().size in 5..10 && r.list == a }, + ) + } + + @Test + @GoSample(method = "sliceSliceFrom") + fun sliceSliceFrom() { + checkDiscoveredProperties( + method = "sliceSliceFrom", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, i: Number, r: GoResult -> + (a.orEmpty().size !in 5..10) && + r.isSuccess && + r.value == null + }, + { a: GoSlice, i: Number, r: GoResult -> + !(a.orEmpty().size !in 5..10) && + (i.toLong() < 0 || i.toLong() > a.orEmpty().size) && + r.isPanic + }, + { a: GoSlice, i: Number, r: GoResult -> + !(a.orEmpty().size !in 5..10) && + !(i.toLong() < 0 || i.toLong() > a.orEmpty().size) && + r.isSuccess && + r.list == a.orEmpty().drop(i.toInt()) + }, + ) + } + + @Test + @GoSample(method = "sliceSliceTo") + fun sliceSliceTo() { + checkDiscoveredProperties( + method = "sliceSliceTo", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, i: Number, r: GoResult -> + (a.orEmpty().size !in 5..10 || i.toLong() > 3) && + r.isSuccess && + r.value == null + }, + { a: GoSlice, i: Number, r: GoResult -> + !(a.orEmpty().size !in 5..10 || i.toLong() > 3) && + (i.toLong() < 0 || i.toLong() > a.orEmpty().size) && + r.isPanic + }, + { a: GoSlice, i: Number, r: GoResult -> + !(a.orEmpty().size !in 5..10 || i.toLong() > 3) && + !(i.toLong() < 0 || i.toLong() > a.orEmpty().size) && + r.isSuccess && + r.list == a.orEmpty().take(i.toInt()) + }, + ) + } + + @Test + @GoSample(method = "sliceSlice") + fun sliceSlice() { + checkDiscoveredProperties( + method = "sliceSlice", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, i: Number, j: Number, r: GoResult -> + (a.orEmpty().size !in 5..10 || i.toLong() == j.toLong()) && + r.isSuccess && + r.value == null + }, + { a: GoSlice, i: Number, j: Number, r: GoResult -> + a.orEmpty().size in 5..10 && + i.toLong() != j.toLong() && + (j.toLong() !in 0..a.orEmpty().size.toLong() || i.toLong() !in 0..j.toLong()) && + r.isPanic + }, + { a: GoSlice, i: Number, j: Number, r: GoResult -> + a.orEmpty().size in 5..10 && + i.toLong() < j.toLong() && + i.toLong() >= 0 && + j.toLong() <= a.orEmpty().size && + r.list == a.orEmpty().subList(i.toInt(), j.toInt()) + }, + ) + } + + @Test + @GoSample(method = "sliceToArrayPointer") + fun sliceToArrayPointer() { + checkDiscoveredProperties( + method = "sliceToArrayPointer", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().size < 3 && r.isPanic }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size >= 3 && + (r.value as? GoPointer)?.value == listOf(a?.get(0), 1L, a?.get(2)) + }, + ) + } + + @Test + @GoSample(method = "sliceCustomToArrayPointer") + fun sliceCustomToArrayPointer() { + checkDiscoveredProperties( + method = "sliceCustomToArrayPointer", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: GoSlice, r: GoResult -> a.orEmpty().size < 3 && r.isPanic }, + { a: GoSlice, r: GoResult -> + a.orEmpty().size >= 3 && + (r.value as? GoPointer)?.value == listOf(a?.get(0), 1L, a?.get(2)) + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SymbolicSliceAliasTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SymbolicSliceAliasTest.kt new file mode 100644 index 0000000000..35d0d800a3 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/SymbolicSliceAliasTest.kt @@ -0,0 +1,22 @@ +package org.usvm.samples.collections.slices + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.eq + +class SymbolicSliceAliasTest : GoNativeTestRunner() { + @Test + @GoSample(method = "symbolicSliceAlias", fixture = "regressions") + fun symbolicSliceAlias() { + checkMatches( + method = "symbolicSliceAlias", + analysisResultsNumberMatcher = eq(count = 2), + { value: Number, r: GoResult -> value.toLong() < 0 && r.long == 3L }, + { value: Number, r: GoResult -> value.toLong() >= 0 && r.long == 7L }, + ) + + replayInputs(methodName = "symbolicSliceAlias", expectedExecutions = 2) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/ConditionsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/ConditionsTest.kt new file mode 100644 index 0000000000..e85523d01c --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/ConditionsTest.kt @@ -0,0 +1,99 @@ +package org.usvm.samples.controlflow + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class ConditionsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "max2") + fun max2() { + checkDiscoveredProperties( + method = "max2", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, b: Number, r: GoResult -> a.toLong() > b.toLong() && r.long == a.toLong() }, + { a: Number, b: Number, r: GoResult -> a.toLong() <= b.toLong() && r.long == b.toLong() }, + ) + } + + @Test + @GoSample(method = "max2Anon") + fun max2Anon() { + checkDiscoveredProperties( + method = "max2Anon", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, b: Number, r: GoResult -> a.toLong() > b.toLong() && r.long == a.toLong() }, + { a: Number, b: Number, r: GoResult -> a.toLong() <= b.toLong() && r.long == b.toLong() }, + ) + } + + @Test + @GoSample(method = "max2Closure") + fun max2Closure() { + checkDiscoveredProperties( + method = "max2Closure", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, b: Number, r: GoResult -> a.toLong() > b.toLong() && r.long == a.toLong() }, + { a: Number, b: Number, r: GoResult -> a.toLong() <= b.toLong() && r.long == b.toLong() }, + ) + } + + @Test + @GoSample(method = "MinPublic") + fun minPublic() { + checkDiscoveredProperties( + method = "MinPublic", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, b: Number, r: GoResult -> r.long == minOf(a.toLong(), b.toLong()) }, + ) + } + + @Test + @GoSample(method = "max3") + fun max3() { + checkDiscoveredProperties( + method = "max3", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, b: Number, c: Number, r: GoResult -> + r.long == maxOf(a.toLong(), b.toLong(), c.toLong()) + }, + ) + } + + @Test + @GoSample(method = "max3Call") + fun max3Call() { + checkDiscoveredProperties( + method = "max3Call", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, b: Number, c: Number, r: GoResult -> + r.long == maxOf(a.toLong(), b.toLong(), c.toLong()) + }, + ) + } + + @Test + @GoSample(method = "max4Call") + fun max4Call() { + checkDiscoveredProperties( + method = "max4Call", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, b: Number, c: Number, d: Number, r: GoResult -> + r.long == maxOf(a.toLong(), b.toLong(), c.toLong(), d.toLong()) + }, + ) + } + + @Test + @GoSample(method = "inc") + fun inc() { + checkDiscoveredProperties( + method = "inc", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, increment: Boolean, r: GoResult -> increment && r.long == nativeInt(a.toLong() + 1) }, + { a: Number, increment: Boolean, r: GoResult -> !increment && r.long == a.toLong() }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/LoopsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/LoopsTest.kt new file mode 100644 index 0000000000..46b3c0420a --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/LoopsTest.kt @@ -0,0 +1,49 @@ +package org.usvm.samples.controlflow + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class LoopsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "loopSimple") + fun loopSimple() { + checkDiscoveredProperties( + method = "loopSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, r: GoResult -> r.long == (maxOf(i.toLong(), 10)) }, + ) + } + + @Test + @GoSample(method = "loopIf") + fun loopIf() { + checkDiscoveredProperties( + method = "loopIf", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, r: GoResult -> + val input = i.toLong() + val expected = when { + input >= 10 -> input + input > 5 && input % 2 == 0L -> 10L + else -> 11L + } + r.long == expected + }, + ) + } + + @Test + @GoSample(method = "loopSum") + fun loopSum() { + checkDiscoveredProperties( + method = "loopSum", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { n: Number, r: GoResult -> + r.long == if (n.toLong() <= 0) 0L else nativeInt(n.toLong() * (n.toLong() + 1) / 2) + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SlowLoopsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SlowLoopsTest.kt new file mode 100644 index 0000000000..1b6355bbad --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SlowLoopsTest.kt @@ -0,0 +1,47 @@ +package org.usvm.samples.controlflow + +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.collatz +import org.usvm.samples.innerSum +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class SlowLoopsTest : GoMethodTestRunner() { + @Tag(value = "manual") + @Test + @GoSample(method = "loopInner") + fun loopInner() { + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkDiscoveredProperties( + method = "loopInner", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, j: Number, r: GoResult -> r.long == innerSum(nativeInt(i.toLong() + j.toLong())) }, + ) + } + + @Tag(value = "manual") + @Test + @GoSample(method = "loopCollatz") + fun loopCollatz() { + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkDiscoveredProperties( + method = "loopCollatz", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, r: GoResult -> r.long == collatz(i.toLong()) }, + ) + } + + @Tag(value = "manual") + @Test + @GoSample(method = "loopInfinite") + fun loopInfinite() { + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkNoResults(method = "loopInfinite") + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SymbolicBranchTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SymbolicBranchTest.kt new file mode 100644 index 0000000000..7959a1b690 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/controlflow/SymbolicBranchTest.kt @@ -0,0 +1,23 @@ +package org.usvm.samples.controlflow + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.eq + +class SymbolicBranchTest : GoNativeTestRunner() { + @Test + @GoSample(method = "symbolicBranch", fixture = "regressions") + fun symbolicBranch() { + checkMatches( + method = "symbolicBranch", + analysisResultsNumberMatcher = eq(count = 3), + { value: Number, r: GoResult -> value.toLong() < 0 && r.long == -1L }, + { value: Number, r: GoResult -> value.toLong() == 0L && r.long == 0L }, + { value: Number, r: GoResult -> value.toLong() > 0 && r.long == 1L }, + ) + + replayInputs(methodName = "symbolicBranch", expectedExecutions = 3) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferTest.kt new file mode 100644 index 0000000000..1fcf47be04 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferTest.kt @@ -0,0 +1,72 @@ +package org.usvm.samples.exceptions + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class DeferTest : GoMethodTestRunner() { + @Test + @GoSample(method = "panicking") + fun panicking() { + checkDiscoveredProperties( + method = "panicking", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.isPanic && r.panicValue == "oh no" }, + ) + } + + @Test + @GoSample(method = "panicRecoverSimple") + fun panicRecoverSimple() { + checkDiscoveredProperties( + method = "panicRecoverSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.long == 0L }, + ) + } + + @Test + @GoSample(method = "panicRecoverResultSimple") + fun panicRecoverResultSimple() { + checkDiscoveredProperties( + method = "panicRecoverResultSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.long == 8L }, + ) + } + + @Test + @GoSample(method = "verySimple") + fun verySimple() { + checkDiscoveredProperties( + method = "verySimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, r: GoResult -> a.toLong() == 3L && r.long == 3L }, + { a: Number, r: GoResult -> a.toLong() != 3L && r.long == 5L }, + ) + } + + @Test + @GoSample(method = "simple") + fun simple() { + checkDiscoveredProperties( + method = "simple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, r: GoResult -> r.long == nativeInt(a.toLong() + 5) }, + ) + } + + @Test + @GoSample(method = "panicRecoverComplex") + fun panicRecoverComplex() { + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkDiscoveredProperties( + method = "panicRecoverComplex", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: Number, r: GoResult -> r.long == -227L }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/globals/GlobalsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/globals/GlobalsTest.kt new file mode 100644 index 0000000000..5c499187ca --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/globals/GlobalsTest.kt @@ -0,0 +1,56 @@ +package org.usvm.samples.globals + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class GlobalsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "globalSimple") + fun globalSimple() { + checkDiscoveredProperties( + method = "globalSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { a: Number, r: GoResult -> a.toLong() == 555L && r.long == 444L }, + { a: Number, r: GoResult -> a.toLong() > 555 && r.long == nativeInt(a.toLong() - 555) }, + { a: Number, r: GoResult -> a.toLong() < 555 && r.long == nativeInt(555 - a.toLong()) }, + ) + } + + @Test + @GoSample(method = "globalArraySimple") + fun globalArraySimple() { + checkDiscoveredProperties( + method = "globalArraySimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, r: GoResult -> i.toLong() !in 0..255 && r.long == -1L }, + { i: Number, r: GoResult -> i.toLong() in 0..255 && r.long == 5L }, + ) + } + + @Test + @GoSample(method = "init") + fun init() { + checkDiscoveredProperties( + method = "init", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.isSuccess }, + ) + } + + @Test + @GoSample(method = "init", fixture = "regressions") + fun regressionPackageInit() { + val runner = object : GoMethodTestRunner(fixture = "regressions") { + fun checkInitializer() = checkDiscoveredProperties( + method = "init", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { result: GoResult -> result.isSuccess }, + ) + } + + runner.checkInitializer() + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/objects/ObjectsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/objects/ObjectsTest.kt new file mode 100644 index 0000000000..87e1321b93 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/objects/ObjectsTest.kt @@ -0,0 +1,88 @@ +package org.usvm.samples.objects + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoPointer +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.field +import org.usvm.samples.longValue +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class ObjectsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "(*usvm/examples.Object).Get") + fun objectGet() { + checkDiscoveredProperties( + method = "(*usvm/examples.Object).Get", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { p: GoPointer?, r: GoResult -> p == null && r.isPanic }, + { p: GoPointer?, r: GoResult -> + p != null && + (p.value as? Map<*, *>).field(index = 0).longValue() == 0L && + r.long == -1L + }, + { p: GoPointer?, r: GoResult -> + p != null && + (p.value as? Map<*, *>).field(index = 0).longValue() != 0L && + r.long == (p.value as? Map<*, *>).field(index = 0).longValue() + }, + ) + } + + @Test + @GoSample(method = "(*usvm/examples.Object).Set") + fun objectSet() { + checkParameterMutations( + method = "(*usvm/examples.Object).Set", + { execution -> execution.arguments[0] == null && execution.result.isPanic }, + { execution -> + val before = execution.arguments[0] as? GoPointer + val after = execution.argumentsAfter[0] as? GoPointer + val value = execution.arguments[1].longValue() + before != null && execution.result.isSuccess && + (after?.value as? Map<*, *>).field(index = 0).longValue() == value + }, + ) + } + + @Test + @GoSample(method = "(*usvm/examples.Object).SetAndReturn") + fun objectSetAndReturn() { + checkDiscoveredProperties( + method = "(*usvm/examples.Object).SetAndReturn", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { p: GoPointer?, _: Number, r: GoResult -> p == null && r.long == -1001L }, + { p: GoPointer?, i: Number, r: GoResult -> + p != null && + i.toLong() == (p.value as? Map<*, *>).field(index = 0).longValue() && + r.long == 1001L + }, + { p: GoPointer?, i: Number, r: GoResult -> + p != null && + i.toLong() != (p.value as? Map<*, *>).field(index = 0).longValue() && + r.long == i.toLong() + }, + ) + } + + @Test + @GoSample(method = "ModifyAndGet") + fun modifyAndGet() { + checkDiscoveredProperties( + method = "ModifyAndGet", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { p: GoPointer?, _: Number, r: GoResult -> p == null && r.long == -1001L }, + { p: GoPointer?, i: Number, r: GoResult -> + p != null && + i.toLong() == (p.value as? Map<*, *>).field(index = 0).longValue() && + r.long == 1001L + }, + { p: GoPointer?, i: Number, r: GoResult -> + p != null && + i.toLong() != (p.value as? Map<*, *>).field(index = 0).longValue() && + r.long == i.toLong() + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointersTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointersTest.kt new file mode 100644 index 0000000000..e1bd69d31e --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointersTest.kt @@ -0,0 +1,50 @@ +package org.usvm.samples.pointers + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoPointer +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class PointersTest : GoMethodTestRunner() { + @Test + @GoSample(method = "pointerSimple") + fun pointerSimple() { + checkDiscoveredProperties( + method = "pointerSimple", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, r: GoResult -> r.long == nativeInt(i.toLong() + 9) }, + ) + } + + @Test + @GoSample(method = "pointerOther") + fun pointerOther() { + checkDiscoveredProperties( + method = "pointerOther", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { i: Number, r: GoResult -> r.long == nativeInt(i.toLong() + 6) }, + ) + } + + @Test + @GoSample(method = "pointerAnother") + fun pointerAnother() { + checkDiscoveredProperties( + method = "pointerAnother", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: Number, r: GoResult -> r.long == 3L }, + ) + } + + @Test + @GoSample(method = "pointerChangeType") + fun pointerChangeType() { + checkDiscoveredProperties( + method = "pointerChangeType", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { pointer: GoPointer?, r: GoResult -> r.isSuccess && r.value == pointer }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/runner/GoMethodTestRunnerTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/runner/GoMethodTestRunnerTest.kt new file mode 100644 index 0000000000..90fb98bb14 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/runner/GoMethodTestRunnerTest.kt @@ -0,0 +1,61 @@ +package org.usvm.samples.runner + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoExecution +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.test.util.checkers.eq +import kotlin.test.assertFailsWith + +class GoMethodTestRunnerTest { + @Test + fun acceptsInputDependentResultsForBothBranches() { + val runner = FixtureRunner(listOf(execution(input = -1L, output = -1L), execution(input = 1L, output = 1L))) + + runner.checkBranchMatches() + } + + @Test + fun requiresEveryExpectedBranchToBeDiscovered() { + val runner = FixtureRunner(listOf(execution(input = -1L, output = -1L))) + + assertFailsWith { runner.checkBranchProperties() } + } + + @Test + fun rejectsAnAdditionalExecutionWithAnIncorrectResult() { + val runner = FixtureRunner( + listOf( + execution(input = -1L, output = -1L), + execution(input = 1L, output = 1L), + execution(input = 2L, output = -1L), + ), + ) + + assertFailsWith { runner.checkBranchProperties() } + } + + private fun execution(input: Long, output: Long): GoExecution = GoExecution( + arguments = listOf(input), + argumentsAfter = listOf(input), + result = GoResult(value = output, isPanic = false), + ) + + private class FixtureRunner(executions: List) : GoMethodTestRunner() { + override val runner = { _: String, _: org.usvm.UMachineOptions -> executions } + + fun checkBranchMatches() = checkMatches( + method = "fixture", + analysisResultsNumberMatcher = eq(count = 2), + { input: Number, result: GoResult -> input.toLong() < 0 && result.long == -1L }, + { input: Number, result: GoResult -> input.toLong() > 0 && result.long == 1L }, + ) + + fun checkBranchProperties() = checkDiscoveredProperties( + method = "fixture", + analysisResultsNumberMatcher = org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults, + { input: Number, result: GoResult -> input.toLong() < 0 && result.long == -1L }, + { input: Number, result: GoResult -> input.toLong() > 0 && result.long == 1L }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt similarity index 87% rename from usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt rename to usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt index 8028cc7df4..2e2f190e98 100644 --- a/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt @@ -1,7 +1,8 @@ -package org.usvm.model +package org.usvm.samples.serialization import org.junit.jupiter.api.Test import org.usvm.generatedGoFile +import org.usvm.model.Parser import java.io.File import kotlin.test.assertEquals diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/strings/NamedStringsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/strings/NamedStringsTest.kt new file mode 100644 index 0000000000..05a85b1378 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/strings/NamedStringsTest.kt @@ -0,0 +1,39 @@ +package org.usvm.samples.strings + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class NamedStringsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "(usvm/examples.errorString).Error") + fun errorStringError() { + checkDiscoveredProperties( + method = "(usvm/examples.errorString).Error", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { s: String, r: GoResult -> r.isSuccess && r.value == "runtime error: $s" }, + ) + } + + @Test + @GoSample(method = "shiftErrorToString") + fun shiftErrorToString() { + checkDiscoveredProperties( + method = "shiftErrorToString", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.isSuccess && r.value == "runtime error: negative shift amount" }, + ) + } + + @Test + @GoSample(method = "appendErrorStrings") + fun appendErrorStrings() { + checkDiscoveredProperties( + method = "appendErrorStrings", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.isSuccess && r.value == "hello, world!" }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/strings/StringConstraintsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/strings/StringConstraintsTest.kt new file mode 100644 index 0000000000..0de9f36273 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/strings/StringConstraintsTest.kt @@ -0,0 +1,92 @@ +package org.usvm.samples.strings + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class StringConstraintsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "checkGoodString") + fun checkGoodString() { + checkDiscoveredProperties( + method = "checkGoodString", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { s: String, r: GoResult -> s.toByteArray().size < 3 && r.isPanic && r.panicValue == "string too small" }, + { s: String, r: GoResult -> + s.toByteArray().size >= 3 && + s.any { it !in 'a'..'z' } && + r.isPanic && + r.panicValue == "bad char" + }, + { s: String, r: GoResult -> s.toByteArray().size >= 3 && s.all { it in 'a'..'z' } && r.isSuccess }, + ) + } + + @Test + @GoSample(method = "stringGetByte") + fun stringGetByte() { + checkDiscoveredProperties( + method = "stringGetByte", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { s: String, i: Number, r: GoResult -> + s.toByteArray().size < 3 && + r.isPanic && + r.panicValue == "string too small" + }, + { s: String, i: Number, r: GoResult -> + s.toByteArray().size >= 3 && + s.any { it !in 'a'..'z' } && + r.isPanic && + r.panicValue == "bad char" + }, + { s: String, i: Number, r: GoResult -> + s.length >= 3 && + s.all { it in 'a'..'z' } && + i.toLong() !in 0.. + s.length >= 3 && + s.all { it in 'a'..'z' } && + i.toLong() in 0.. + s.toByteArray().size < 3 && + r.isPanic && + r.panicValue == "string too small" + }, + { s: String, i: Number, r: GoResult -> + s.toByteArray().size >= 3 && + s.any { it !in 'a'..'z' } && + r.isPanic && + r.panicValue == "bad char" + }, + { s: String, i: Number, r: GoResult -> + s.length >= 3 && + s.all { it in 'a'..'z' } && + i.toLong() !in 0.. + s.length >= 3 && + s.all { it in 'a'..'z' } && + i.toLong() in 0.. r.isSuccess && r.value == "hello, world" }, + ) + } + + @Test + @GoSample(method = "stringFromByteArray") + fun stringFromByteArray() { + checkDiscoveredProperties( + method = "stringFromByteArray", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.isSuccess && r.value == "hello" }, + ) + } + + @Test + @GoSample(method = "stringToByteArray") + fun stringToByteArray() { + checkDiscoveredProperties( + method = "stringToByteArray", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> + r.list == listOf(104.toUByte(), 101.toUByte(), 108.toUByte(), 108.toUByte(), 111.toUByte()) + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt new file mode 100644 index 0000000000..ea4cbe95b8 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt @@ -0,0 +1,118 @@ +package org.usvm.samples.types + +import org.jacodb.go.api.PointerType +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoInterfaceValue +import org.usvm.interpreter.GoPointer +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.field +import org.usvm.samples.isNamed +import org.usvm.samples.longValue +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class InterfacesTest : GoMethodTestRunner() { + @Test + @GoSample(method = "assertCreature") + fun assertCreature() { + checkDiscoveredProperties( + method = "assertCreature", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.list == listOf(mapOf("field0" to "Name", "field1" to 42L), true) }, + ) + } + + @Test + @GoSample(method = "assertCreatureNoComma") + fun assertCreatureNoComma() { + checkDiscoveredProperties( + method = "assertCreatureNoComma", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.map == mapOf("field0" to "Name", "field1" to 42L) }, + ) + } + + @Test + @GoSample(method = "assertCreaturePointer") + fun assertCreaturePointer() { + checkDiscoveredProperties( + method = "assertCreaturePointer", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.list == listOf(GoPointer(mapOf("field0" to "Name", "field1" to 42L)), true) }, + ) + } + + @Test + @GoSample(method = "assertCreatureFailNoComma") + fun assertCreatureFailNoComma() { + checkDiscoveredProperties( + method = "assertCreatureFailNoComma", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.isPanic }, + ) + } + + @Test + @GoSample(method = "assertIntAny") + fun assertIntAny() { + checkDiscoveredProperties( + method = "assertIntAny", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { r: GoResult -> r.long == 5L }, + ) + } + + @Test + @GoSample(method = "callCreature") + fun callCreature() { + checkDiscoveredProperties( + method = "callCreature", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { c: GoInterfaceValue?, r: GoResult -> c == null && r.long == -1L }, + { c: GoInterfaceValue?, r: GoResult -> c?.type is PointerType && c.value == null && r.isPanic }, + { c: GoInterfaceValue?, r: GoResult -> + val receiver = when (val value = c?.value) { + is GoPointer -> value.value as? Map<*, *> + is Map<*, *> -> value + else -> null + } + c != null && receiver != null && r.long == receiver.field(index = 1).longValue() + }, + ) + } + + @Test + @GoSample(method = "assertCreatureArgument") + fun assertCreatureArgument() { + checkDiscoveredProperties( + method = "assertCreatureArgument", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { c: GoInterfaceValue?, r: GoResult -> + c?.isNamed(name = "usvm/examples.Person") == true && + r.list == listOf(c.value, true) + }, + { c: GoInterfaceValue?, r: GoResult -> + c?.isNamed(name = "usvm/examples.Person") != true && + r.list == listOf(mapOf("field0" to "", "field1" to 0L), false) + }, + ) + } + + @Test + @GoSample(method = "assertCreatureArgumentCall") + fun assertCreatureArgumentCall() { + checkDiscoveredProperties( + method = "assertCreatureArgumentCall", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { c: GoInterfaceValue?, r: GoResult -> + c?.isNamed(name = "usvm/examples.Person") == true && + r.long == (c.value as? Map<*, *>).field(index = 1).longValue() + }, + { c: GoInterfaceValue?, r: GoResult -> + c?.isNamed(name = "usvm/examples.Person") != true && + r.long == -1L + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/types/NamedTypesTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/types/NamedTypesTest.kt new file mode 100644 index 0000000000..3077ec4004 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/NamedTypesTest.kt @@ -0,0 +1,81 @@ +package org.usvm.samples.types + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoInterfaceValue +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.isNamed +import org.usvm.samples.longValue +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults +import org.usvm.type.GoBasicTypes + +class NamedTypesTest : GoMethodTestRunner() { + @Test + @GoSample(method = "(usvm/examples.NamedInt).square") + fun namedIntsquare() { + checkDiscoveredProperties( + method = "(usvm/examples.NamedInt).square", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { n: Number, r: GoResult -> n.toLong() == 0L && r.isPanic && r.panicValue == "zero" }, + { n: Number, r: GoResult -> n.toLong() in listOf(-1L, 1L) && r.isPanic && r.panicValue == "one" }, + { n: Number, r: GoResult -> n.toLong() !in -1..1 && r.long == nativeInt(n.toLong() * n.toLong()) }, + ) + } + + @Test + @GoSample(method = "callNamedInt") + fun callNamedInt() { + checkDiscoveredProperties( + method = "callNamedInt", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { n: Number, r: GoResult -> n.toLong() == 0L && r.isPanic && r.panicValue == "zero" }, + { n: Number, r: GoResult -> n.toLong() in listOf(-1L, 1L) && r.isPanic && r.panicValue == "one" }, + { n: Number, r: GoResult -> n.toLong() !in -1..1 && r.long == nativeInt(n.toLong() * n.toLong()) }, + ) + } + + @Test + @GoSample(method = "toNamedInt") + fun toNamedInt() { + checkDiscoveredProperties( + method = "toNamedInt", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { n: Number, r: GoResult -> r.long == nativeInt(n.toLong() + 1) }, + ) + } + + @Test + @GoSample(method = "assertNamedIntCall") + fun assertNamedIntCall() { + checkDiscoveredProperties( + method = "assertNamedIntCall", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { n: GoInterfaceValue?, r: GoResult -> + n?.isNamed(name = "usvm/examples.NamedInt") == true && + n.value.longValue() in -1..1 && + r.isPanic + }, + { n: GoInterfaceValue?, r: GoResult -> + n?.isNamed(name = "usvm/examples.NamedInt") == true && + n.value.longValue() !in -1..1 && + r.long == nativeInt(n.value.longValue() * n.value.longValue()) + }, + { n: GoInterfaceValue?, r: GoResult -> + n?.type == GoBasicTypes.INT && + nativeInt(n.value.longValue() * n.value.longValue()) <= 1 && + r.isPanic + }, + { n: GoInterfaceValue?, r: GoResult -> + n?.type == GoBasicTypes.INT && + nativeInt(n.value.longValue() * n.value.longValue()) > 1 && + r.long == nativeInt(n.value.longValue() * n.value.longValue()) + }, + { n: GoInterfaceValue?, r: GoResult -> + n?.isNamed(name = "usvm/examples.NamedInt") != true && + n?.type != GoBasicTypes.INT && + r.long == -1L + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/types/StructsTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/types/StructsTest.kt new file mode 100644 index 0000000000..79e20d033c --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/StructsTest.kt @@ -0,0 +1,100 @@ +package org.usvm.samples.types + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoInterfaceValue +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoStruct +import org.usvm.samples.field +import org.usvm.samples.longValue +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class StructsTest : GoMethodTestRunner() { + @Test + @GoSample(method = "GetAge") + fun getAge() { + checkDiscoveredProperties( + method = "GetAge", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { p: GoStruct, r: GoResult -> r.long == p.field(index = 1).longValue() }, + ) + } + + @Test + @GoSample(method = "(usvm/examples.Person).GetAge") + fun personGetAge() { + checkDiscoveredProperties( + method = "(usvm/examples.Person).GetAge", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { p: GoStruct, r: GoResult -> r.long == p.field(index = 1).longValue() }, + ) + } + + @Test + @GoSample(method = "(usvm/examples.Person).GetName") + fun personGetName() { + checkDiscoveredProperties( + method = "(usvm/examples.Person).GetName", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { p: GoStruct, r: GoResult -> r.isSuccess && r.value == p.field(index = 0) }, + ) + } + + @Test + @GoSample(method = "(usvm/examples.Person).Validate") + fun personValidate() { + checkDiscoveredProperties( + method = "(usvm/examples.Person).Validate", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: GoStruct, r: GoResult -> r.list == listOf(true, null) }, + ) + } + + @Test + @GoSample(method = "(usvm/examples.Person).WithName") + fun personWithName() { + checkDiscoveredProperties( + method = "(usvm/examples.Person).WithName", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: GoStruct, name: String, r: GoResult -> + name.toByteArray().size < 3 && + r.isPanic && + r.panicValue == "string too small" + }, + { _: GoStruct, name: String, r: GoResult -> + name.toByteArray().size >= 3 && + name.any { it !in 'a'..'z' } && + r.isPanic && + r.panicValue == "bad char" + }, + { p: GoStruct, name: String, r: GoResult -> + name.length >= 3 && + name.all { it in 'a'..'z' } && + r.isSuccess && + (r.value as? GoInterfaceValue)?.value == mapOf("field0" to name, "field1" to p.field(index = 1)) + }, + ) + } + + @Test + @GoSample(method = "nameSmall") + fun nameSmall() { + checkDiscoveredProperties( + method = "nameSmall", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { p: GoStruct, r: GoResult -> (p.field(index = 0) as String).toByteArray().size < 3 && r.isPanic }, + { p: GoStruct, r: GoResult -> + (p.field(index = 0) as String).length >= 3 && + (p.field(index = 0) as String).any { it !in 'a'..'z' } && + r.isPanic + }, + { p: GoStruct, r: GoResult -> + (p.field(index = 0) as String).length >= 3 && + (p.field(index = 0) as String).all { it in 'a'..'z' } && + r.isSuccess && + r.value == ((p.field(index = 0) as String).length < 5) + }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt new file mode 100644 index 0000000000..332c199ae7 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt @@ -0,0 +1,35 @@ +package org.usvm.samples.unsupported + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.usvm.samples.GoMethodTestRunner +import org.usvm.samples.GoSample +import kotlin.test.assertTrue + +class GoUnsupportedTest : GoMethodTestRunner(fixture = "regressions") { + @Test + @GoSample(method = "unsupportedGoroutine", fixture = "regressions") + fun goroutinesAreRejectedAtEntry() = assertUnsupported( + method = "unsupportedGoroutine", + message = "unsupported instructions" + ) + + @Test + @GoSample(method = "unsupportedCaller", fixture = "regressions") + fun goroutinesAreRejectedInsideCalls() = assertUnsupported( + method = "unsupportedCaller", + message = "unsupported instructions" + ) + + @Test + @GoSample(method = "symbolicStringEquality", fixture = "regressions") + fun twoSymbolicStringLengthsAreRejected() = assertUnsupported( + method = "symbolicStringEquality", + message = "two symbolic lengths" + ) + + private fun assertUnsupported(method: String, message: String) { + val error = assertThrows { runner(method, options) } + assertTrue(error.message.orEmpty().contains(message), error.message) + } +} diff --git a/usvm-go/src/test/resources/logback.xml b/usvm-go/src/test/resources/logback.xml index cf11c9cba0..21170a981d 100644 --- a/usvm-go/src/test/resources/logback.xml +++ b/usvm-go/src/test/resources/logback.xml @@ -6,7 +6,7 @@ - + \ No newline at end of file From 672c2b76e3cfa4cca2c1eaf587d070af19022035 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Fri, 9 Oct 2026 14:46:29 +0300 Subject: [PATCH 3/6] Fix nil map operations and missing lookup values --- usvm-go/README.md | 22 ++-- usvm-go/src/main/go/regressions/semantics.go | 66 ++++++++++++ .../src/main/go/regressions/semantics_test.go | 100 ++++++++++-------- .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 48 ++++++--- .../collections/maps/MapRegressionTest.kt | 64 ++++++++++- 5 files changed, 230 insertions(+), 70 deletions(-) diff --git a/usvm-go/README.md b/usvm-go/README.md index 189c5c5ffe..a8eff83c2f 100644 --- a/usvm-go/README.md +++ b/usvm-go/README.md @@ -56,7 +56,7 @@ retain structured values and interface dynamic types; these snapshots do not pre object identity or the complete alias graph. Function parameters remain mocked: the `call` example checks that mock contract, rather than replaying an actual function body. -The thematic `*RegressionTest` classes retain comparisons against native Go for 46 +The thematic `*RegressionTest` classes retain comparisons against native Go for 54 zero-argument scalar/panic cases. `SymbolicBranchTest` and `SymbolicSliceAliasTest` also check input-dependent properties, then replay five generated concrete inputs in a native Go executable. Native scalar comparisons currently use textual representations; native @@ -75,7 +75,7 @@ instruction coverage but still enforce their semantic expectations. The stronger symbolic expectations expose unresolved input-model and semantic defects. They are kept as failing tests, without disabled tests or expected-failure wrappers. -The current local run has **156 default tests: 102 passed, 54 failed**, plus +The current local run has **165 default tests: 120 passed, 45 failed**, plus **5 manual tests: 3 passed, 2 failed**, with no skipped tests. Detekt on Go main/test sources and the project-list check pass. Failures are grouped as follows: @@ -83,7 +83,7 @@ sources and the project-list check pass. Failures are grouped as follows: | --- | ---: | | `collections.slices` | 20 | | `types` | 13 | -| `collections.maps` | 9 | +| `collections.maps` | 0 | | `objects` | 4 | | `strings` | 4 | | `algorithms` | 2 | @@ -93,10 +93,14 @@ sources and the project-list check pass. Failures are grouped as follows: The manual failures are `canVisitAllRooms` and `mapLoopLen`. Counts are local-run observations, not a count of independent bugs or an exhaustive list; budgeted symbolic exploration and model materialization may affect which witnesses are collected. This draft integration -requires those failures to be resolved before merging. A concrete -native comparison is `nilMapLookup`: Go returns zero for a missing key in a nil map, -while USVM currently produces panic. This diagnostic permits partial instruction -coverage to expose the value/panic mismatch; it still requires one matching execution. +requires those failures to be resolved before merging. + +Nine previously failing default map tests now pass. New native regressions check +nil-map lookup, comma-ok, deletion, range and assignment panic with integer keys/values, +including lookup/deletion/range on named maps. A symbolic comma-ok regression requires +witnesses for nil maps, absent keys and present keys, with input-dependent values. +The two constant nil-map range tests permit partial instruction coverage because their +loop bodies are unreachable; they still require exactly one native-matching execution. `generateGoImports` can export the import examples for investigation. The original import/standard-library exploratory factories depended on manually prepared dumps; @@ -124,8 +128,8 @@ support. In particular: - Unknown external calls and function parameters retain the prototype's mocking behavior. `GoFunctionReference` identifies a function input that cannot be replayed. - General array/struct value-copy behavior, pointer/interface equality, reference - map keys and nil-map operations need further semantic validation. The native - regression suite does not claim coverage of these areas. + map keys and zero values for composite/named map values need further semantic + validation. Nil-map regressions currently cover integer keys/values. - Package loading, standard-library integration and arbitrary repository workflows have not been validated end to end. Instruction coverage alone is not a semantic correctness check. diff --git a/usvm-go/src/main/go/regressions/semantics.go b/usvm-go/src/main/go/regressions/semantics.go index 5ce6ee9e59..dea41872aa 100644 --- a/usvm-go/src/main/go/regressions/semantics.go +++ b/usvm-go/src/main/go/regressions/semantics.go @@ -285,3 +285,69 @@ func nilMapLookup() int { var values map[int]int return values[7] } + +func nilMapLookupComma() bool { + var values map[int]int + _, ok := values[7] + return ok +} + +func nilMapDelete() int { + var values map[int]int + delete(values, 7) + return len(values) +} + +func nilMapAssignment() int { + var values map[int]int + values[7] = 42 + return values[7] +} + +type namedMap map[int]int + +func nilNamedMapLookup() int { + var values namedMap + return values[7] +} + +func nilNamedMapDelete() int { + var values namedMap + delete(values, 7) + return len(values) +} + +func missingMapLookupCommaValue() int { + values := map[int]int{7: 42} + value, _ := values[8] + return value +} + +func symbolicMapLookupComma(values map[int]int, key int) (int, bool) { + value, ok := values[key] + if values == nil { + return value, ok + } + if ok { + return value, true + } + return value, false +} + +func nilMapRange() int { + var values map[int]int + iterations := 0 + for range values { + iterations++ + } + return iterations +} + +func nilNamedMapRange() int { + var values namedMap + iterations := 0 + for range values { + iterations++ + } + return iterations +} diff --git a/usvm-go/src/main/go/regressions/semantics_test.go b/usvm-go/src/main/go/regressions/semantics_test.go index 4615024dce..b7b1b261fe 100644 --- a/usvm-go/src/main/go/regressions/semantics_test.go +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -9,52 +9,60 @@ import ( func TestNativeOracle(t *testing.T) { cases := map[string]func() any{ - "nilMapLookup": func() any { return nilMapLookup() }, - "unsignedResultWidth": func() any { return unsignedResultWidth() }, - "shiftByBitWidth": func() any { return shiftByBitWidth() }, - "bitwiseComplement": func() any { return bitwiseComplement() }, - "bitwiseAndNot": func() any { return bitwiseAndNot() }, - "nativeIntWidth": func() any { return nativeIntWidth() }, - "sliceCapacity": func() any { return sliceCapacity() }, - "mapHintLength": func() any { return mapHintLength() }, - "mapInsertLength": func() any { return mapInsertLength() }, - "sliceAlias": func() any { return sliceAlias() }, - "stringEquality": func() any { return stringEquality() }, - "stringInequality": func() any { return stringInequality() }, - "stringLengthDifference": func() any { return stringLengthDifference() }, - "stringOrdering": func() any { return stringOrdering() }, - "stringPrefixOrdering": func() any { return stringPrefixOrdering() }, - "emptyStringEquality": func() any { return emptyStringEquality() }, - "stringConcatenation": func() any { return stringConcatenation() }, - "stringBytesCopy": func() any { return stringBytesCopy() }, - "bytesStringCopy": func() any { return bytesStringCopy() }, - "sliceOffsetAlias": func() any { return sliceOffsetAlias() }, - "sliceFullCapacity": func() any { return sliceFullCapacity() }, - "sliceCopyOffset": func() any { return sliceCopyOffset() }, - "sliceCopyString": func() any { return sliceCopyString() }, - "sliceAppendReuse": func() any { return sliceAppendReuse() }, - "sliceAppendAllocate": func() any { return sliceAppendAllocate() }, - "sliceAppendOffset": func() any { return sliceAppendOffset() }, - "sliceArrayPointerAlias": func() any { return sliceArrayPointerAlias() }, - "nilSliceLength": func() any { return nilSliceLength() }, - "nilSliceAppend": func() any { return nilSliceAppend() }, - "mapOverwriteLength": func() any { return mapOverwriteLength() }, - "mapDeleteLength": func() any { return mapDeleteLength() }, - "bitwiseAndOr": func() any { return bitwiseAndOr() }, - "oversizedShiftCount": func() any { return oversizedShiftCount() }, - "signedRightShift": func() any { return signedRightShift() }, - "unsignedRightShift": func() any { return unsignedRightShift() }, - "nativeIntOverflow": func() any { return nativeIntOverflow() }, - "unsignedWidening": func() any { return unsignedWidening() }, - "oversizedIndex": func() any { return oversizedIndex() }, - "negativeIndex": func() any { return negativeIndex() }, - "negativeShift": func() any { return negativeShift() }, - "divideByZero": func() any { return divideByZero() }, - "remainderByZero": func() any { return remainderByZero() }, - "utf8StringLength": func() any { return utf8StringLength() }, - "unsignedSliceLength": func() any { return unsignedSliceLength() }, - "narrowIndex": func() any { return narrowIndex() }, - "negativeSliceHigh": func() any { return negativeSliceHigh() }, + "nilMapLookupComma": func() any { return nilMapLookupComma() }, + "nilMapDelete": func() any { return nilMapDelete() }, + "nilMapAssignment": func() any { return nilMapAssignment() }, + "nilNamedMapLookup": func() any { return nilNamedMapLookup() }, + "nilNamedMapDelete": func() any { return nilNamedMapDelete() }, + "missingMapLookupCommaValue": func() any { return missingMapLookupCommaValue() }, + "nilMapRange": func() any { return nilMapRange() }, + "nilNamedMapRange": func() any { return nilNamedMapRange() }, + "nilMapLookup": func() any { return nilMapLookup() }, + "unsignedResultWidth": func() any { return unsignedResultWidth() }, + "shiftByBitWidth": func() any { return shiftByBitWidth() }, + "bitwiseComplement": func() any { return bitwiseComplement() }, + "bitwiseAndNot": func() any { return bitwiseAndNot() }, + "nativeIntWidth": func() any { return nativeIntWidth() }, + "sliceCapacity": func() any { return sliceCapacity() }, + "mapHintLength": func() any { return mapHintLength() }, + "mapInsertLength": func() any { return mapInsertLength() }, + "sliceAlias": func() any { return sliceAlias() }, + "stringEquality": func() any { return stringEquality() }, + "stringInequality": func() any { return stringInequality() }, + "stringLengthDifference": func() any { return stringLengthDifference() }, + "stringOrdering": func() any { return stringOrdering() }, + "stringPrefixOrdering": func() any { return stringPrefixOrdering() }, + "emptyStringEquality": func() any { return emptyStringEquality() }, + "stringConcatenation": func() any { return stringConcatenation() }, + "stringBytesCopy": func() any { return stringBytesCopy() }, + "bytesStringCopy": func() any { return bytesStringCopy() }, + "sliceOffsetAlias": func() any { return sliceOffsetAlias() }, + "sliceFullCapacity": func() any { return sliceFullCapacity() }, + "sliceCopyOffset": func() any { return sliceCopyOffset() }, + "sliceCopyString": func() any { return sliceCopyString() }, + "sliceAppendReuse": func() any { return sliceAppendReuse() }, + "sliceAppendAllocate": func() any { return sliceAppendAllocate() }, + "sliceAppendOffset": func() any { return sliceAppendOffset() }, + "sliceArrayPointerAlias": func() any { return sliceArrayPointerAlias() }, + "nilSliceLength": func() any { return nilSliceLength() }, + "nilSliceAppend": func() any { return nilSliceAppend() }, + "mapOverwriteLength": func() any { return mapOverwriteLength() }, + "mapDeleteLength": func() any { return mapDeleteLength() }, + "bitwiseAndOr": func() any { return bitwiseAndOr() }, + "oversizedShiftCount": func() any { return oversizedShiftCount() }, + "signedRightShift": func() any { return signedRightShift() }, + "unsignedRightShift": func() any { return unsignedRightShift() }, + "nativeIntOverflow": func() any { return nativeIntOverflow() }, + "unsignedWidening": func() any { return unsignedWidening() }, + "oversizedIndex": func() any { return oversizedIndex() }, + "negativeIndex": func() any { return negativeIndex() }, + "negativeShift": func() any { return negativeShift() }, + "divideByZero": func() any { return divideByZero() }, + "remainderByZero": func() any { return remainderByZero() }, + "utf8StringLength": func() any { return utf8StringLength() }, + "unsignedSliceLength": func() any { return unsignedSliceLength() }, + "narrowIndex": func() any { return narrowIndex() }, + "negativeSliceHigh": func() any { return negativeSliceHigh() }, } results := make(map[string]string, len(cases)) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index 4318ae18eb..5028c177ab 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -485,15 +485,15 @@ 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 reference = unboxNamedRef(expr.instance.accept(this).asExpr(ctx.addressSort), expr.instance.type) + val map = mapOrEmpty(reference, 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 GoStepAbort() scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw GoStepAbort() val contains = scope.calcOnState { @@ -508,13 +508,17 @@ class GoExprVisitor( } else { UMapEntryLValue(key.sort, valueSort, map, key.asExpr(key.sort), mapType, USizeExprKeyInfo()) } - val rvalue = scope.calcOnState { memory.read(lvalue).asExpr(valueSort) } + val value = ctx.mkIte( + contains, + trueBranch = { scope.calcOnState { memory.read(lvalue).asExpr(valueSort) } }, + falseBranch = { valueSort.sampleUValue() } + ) return scope.calcOnState { if (commaOk) { - mkTuple(TupleType(listOf(mapType.valueType, GoBasicTypes.BOOL)), rvalue, contains) + mkTuple(TupleType(listOf(mapType.valueType, GoBasicTypes.BOOL)), value, contains) } else { - ctx.mkIte(contains, { rvalue }, { rvalue.sort.sampleUValue() }) + value } } } @@ -528,7 +532,7 @@ class GoExprVisitor( val collection = expr.instance.accept(this).asExpr(ctx.addressSort).let { when (val type = expr.instance.type.underlying()) { is MapType -> { - copyMap(it, type) + copyMap(unboxNamedRef(it, expr.instance.type), type) } GoBasicTypes.STRING -> { it @@ -781,9 +785,28 @@ class GoExprVisitor( return expr } - return unbox(expr, ctx.typeToSort(type.underlying())).asExpr(ctx.addressSort) + if (type.underlying() !is MapType) { + return unbox(expr, ctx.typeToSort(type.underlying())).asExpr(ctx.addressSort) + } + + // Named maps may be nil, while every non-nil reference must carry the boxing tag. + val boxed = ctx.mkIte( + ctx.mkHeapRefEq(expr, ctx.nullRef), + trueBranch = { box(ctx.nullRef, type) }, + falseBranch = { expr } + ) + scope.assert(isBoxed(boxed)) ?: throw GoStepAbort() + + return scope.calcOnState { unbox(boxed, ctx.addressSort).asExpr(ctx.addressSort) } } + private fun mapOrEmpty(map: UHeapRef, type: MapType): UHeapRef = ctx.mkIte( + ctx.mkHeapRefEq(map, ctx.nullRef), + // Core map operations require a non-nil reference; this temporary map does not escape. + trueBranch = { scope.calcOnState { mkSymbolicObjectMap(type) } }, + falseBranch = { map } + ) + @Suppress("ThrowsCount") // Failed scope checks stop this hop. private fun visitGoBinaryExpr(expr: GoBinaryExpr): UExpr { if (expr.lhv.type.underlying() == GoBasicTypes.STRING && expr.rhv.type.underlying() == GoBasicTypes.STRING) { @@ -1077,9 +1100,10 @@ class GoExprVisitor( } "delete" -> { - val map = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) + val reference = 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 map = mapOrEmpty(reference, mapType) val keySort = ctx.typeToSort(mapType.keyType) scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw GoStepAbort() @@ -1153,8 +1177,8 @@ class GoExprVisitor( srcMap: UHeapRef, mapType: MapType, ): UHeapRef = with(ctx) { - checkNotNull(srcMap) ?: throw GoStepAbort() - scope.ensureObjectMapSizeCorrect(srcMap, mapType) ?: throw GoStepAbort() + val source = mapOrEmpty(srcMap, mapType) + scope.ensureObjectMapSizeCorrect(source, mapType) ?: throw GoStepAbort() val keySort = typeToSort(mapType.keyType) val valueSort = typeToSort(mapType.valueType) @@ -1162,9 +1186,9 @@ class GoExprVisitor( return scope.calcOnState { val destMap = mkSymbolicObjectMap(mapType) if (isRefSet) { - symbolicObjectMapMergeInto(destMap, srcMap, mapType, valueSort) + symbolicObjectMapMergeInto(destMap, source, mapType, valueSort) } else { - symbolicPrimitiveMapCopyIntoEmpty(destMap, srcMap, mapType, keySort, valueSort, USizeExprKeyInfo()) + symbolicPrimitiveMapCopyIntoEmpty(destMap, source, mapType, keySort, valueSort, USizeExprKeyInfo()) } destMap } diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt index 43ac5c2b21..77072cfa9c 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt @@ -1,8 +1,13 @@ package org.usvm.samples.collections.maps import org.junit.jupiter.api.Test +import org.usvm.samples.GoMap import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult import org.usvm.samples.GoSample +import org.usvm.samples.hasLongKey +import org.usvm.samples.longEntry +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults class MapRegressionTest : GoNativeTestRunner() { @Test @@ -23,10 +28,63 @@ class MapRegressionTest : GoNativeTestRunner() { @Test @GoSample(method = "nilMapLookup", fixture = "regressions") - fun nilMapLookup() { - // Report the return/panic mismatch even when the frontend cannot cover the return instruction. + fun nilMapLookup() = checkNative(method = "nilMapLookup") + + @Test + @GoSample(method = "nilMapLookupComma", fixture = "regressions") + fun nilMapLookupComma() = checkNative(method = "nilMapLookupComma") + + @Test + @GoSample(method = "nilMapDelete", fixture = "regressions") + fun nilMapDelete() = checkNative(method = "nilMapDelete") + + @Test + @GoSample(method = "nilMapAssignment", fixture = "regressions") + fun nilMapAssignment() = checkNative(method = "nilMapAssignment") + + @Test + @GoSample(method = "nilNamedMapLookup", fixture = "regressions") + fun nilNamedMapLookup() = checkNative(method = "nilNamedMapLookup") + + @Test + @GoSample(method = "nilNamedMapDelete", fixture = "regressions") + fun nilNamedMapDelete() = checkNative(method = "nilNamedMapDelete") + + @Test + @GoSample(method = "missingMapLookupCommaValue", fixture = "regressions") + fun missingMapLookupCommaValue() = checkNative(method = "missingMapLookupCommaValue") + + @Test + @GoSample(method = "nilMapRange", fixture = "regressions") + fun nilMapRange() { + // The loop body is unreachable for a nil map. machineOptions = machineOptions.copy(failOnNotFullCoverage = false) - checkNative(method = "nilMapLookup") + checkNative(method = "nilMapRange") + } + + @Test + @GoSample(method = "nilNamedMapRange", fixture = "regressions") + fun nilNamedMapRange() { + // The loop body is unreachable for a nil map. + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + checkNative(method = "nilNamedMapRange") + } + + @Test + @GoSample(method = "symbolicMapLookupComma", fixture = "regressions") + fun symbolicMapLookupComma() { + checkDiscoveredProperties( + method = "symbolicMapLookupComma", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { m: GoMap, _: Number, r: GoResult -> m == null && r.list == listOf(0L, false) }, + { m: GoMap, key: Number, r: GoResult -> + m != null && !m.hasLongKey(key.toLong()) && r.list == listOf(0L, false) + }, + { m: GoMap, key: Number, r: GoResult -> + m.hasLongKey(key.toLong()) && r.list == listOf(m.longEntry(key.toLong()), true) + }, + ) } } From 0fa5b8d047dde9ae0e60bd9a1b1c2192e4121456 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Fri, 9 Oct 2026 15:17:23 +0300 Subject: [PATCH 4/6] Fix named numeric values and numeric conversions --- usvm-go/README.md | 25 +++- usvm-go/src/main/go/pack.go | 20 ++- usvm-go/src/main/go/regressions/semantics.go | 86 ++++++++++++ .../src/main/go/regressions/semantics_test.go | 14 ++ .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 123 ++++++++++++++---- .../org/usvm/interpreter/GoTestInterpreter.kt | 19 ++- .../arithmetic/ArithmeticRegressionTest.kt | 101 ++++++++++++++ 7 files changed, 349 insertions(+), 39 deletions(-) diff --git a/usvm-go/README.md b/usvm-go/README.md index a8eff83c2f..5c514a6aa2 100644 --- a/usvm-go/README.md +++ b/usvm-go/README.md @@ -56,10 +56,9 @@ retain structured values and interface dynamic types; these snapshots do not pre object identity or the complete alias graph. Function parameters remain mocked: the `call` example checks that mock contract, rather than replaying an actual function body. -The thematic `*RegressionTest` classes retain comparisons against native Go for 54 -zero-argument scalar/panic cases. `SymbolicBranchTest` and `SymbolicSliceAliasTest` also -check input-dependent properties, then replay five generated concrete inputs in a native -Go executable. Native scalar comparisons currently use textual representations; native +The thematic `*RegressionTest` classes retain comparisons against native Go for 66 +zero-argument scalar/panic cases. Branch, slice-alias and named-number/interface +checks also replay seven generated concrete inputs in a native Go executable. Native scalar comparisons currently use textual representations; native panic comparisons check occurrence, while symbolic sample expectations may also check the payload. Native replay does not yet support arbitrary collection/struct inputs. @@ -75,14 +74,14 @@ instruction coverage but still enforce their semantic expectations. The stronger symbolic expectations expose unresolved input-model and semantic defects. They are kept as failing tests, without disabled tests or expected-failure wrappers. -The current local run has **165 default tests: 120 passed, 45 failed**, plus +The current local run has **181 default tests: 138 passed, 43 failed**, plus **5 manual tests: 3 passed, 2 failed**, with no skipped tests. Detekt on Go main/test sources and the project-list check pass. Failures are grouped as follows: | Test package | Default failures | | --- | ---: | | `collections.slices` | 20 | -| `types` | 13 | +| `types` | 11 | | `collections.maps` | 0 | | `objects` | 4 | | `strings` | 4 | @@ -102,13 +101,22 @@ witnesses for nil maps, absent keys and present keys, with input-dependent value The two constant nil-map range tests permit partial instruction coverage because their loop bodies are unreachable; they still require exactly one native-matching execution. +All 34 arithmetic/named-number tests now pass. Sixteen new regressions cover +non-nil named scalar inputs, unary operators and argument snapshots, numeric +interface assertions/round trips, unsigned integer-to-float conversion, finite +representable float-to-integer truncation, and exact float-literal export. +Twelve compare constant results with native Go; four have symbolic expectations. +The interface round-trip test also replays two generated inputs and checks that +passing a numeric argument by value preserves its original snapshot. + `generateGoImports` can export the import examples for investigation. The original import/standard-library exploratory factories depended on manually prepared dumps; they are not included in the default suite. ## Representation and boundaries -The exporter records the target `int` width and raw string bytes. Arrays and slices +The exporter records the target `int` width, raw string bytes, and floating-point +constants with enough digits to reproduce their float32/float64 values. Arrays and slices share storage by element type. Slice headers record backing storage, offset, length and capacity; `copy` and `append` operate on these views. Go pointers to fields or array elements are frontend-local metadata, without changing core expression @@ -122,6 +130,9 @@ support. In particular: - String comparison requires at least one concrete length. Rune conversion and complete UTF-8 string iteration are not implemented. Resolved strings are decoded as UTF-8, so arbitrary invalid byte sequences are not preserved in result text. +- Numeric conversion regressions cover finite, representable float-to-integer + inputs. NaN, infinity and out-of-range float-to-integer results have not been + validated; such results may depend on the Go target. - Collection sizes use a nonnegative BV32 domain; symbolic sizes are restricted to that domain. Input slices currently model capacity as length, and materialized array/slice/string models are capped at 10,000 elements. diff --git a/usvm-go/src/main/go/pack.go b/usvm-go/src/main/go/pack.go index d5280d890c..7e55e7b0ca 100644 --- a/usvm-go/src/main/go/pack.go +++ b/usvm-go/src/main/go/pack.go @@ -6,6 +6,7 @@ import ( "go/types" "log" "sort" + "strconv" "strings" "github.com/samber/lo" @@ -168,7 +169,7 @@ func (p *Package) PackMember(in ssa.Member) Member { common.Type = NamedConstMember return NamedConst{ CommonMember: common, - Value: packConstant(member.Value.Type().String(), member.Value.Value), + Value: packConstant(member.Value.Type(), member.Value.Value), } case *ssa.Global: p.AddType(member.Type()) @@ -590,7 +591,7 @@ func (p *Package) PackValue(in ssa.Value) Value { common.Type = ConstValue return Const{ CommonValue: common, - Value: packConstant(value.Type().String(), value.Value), + Value: packConstant(value.Type(), value.Value), } case *ssa.Global: common.Type = GlobalValue @@ -701,12 +702,23 @@ type WithMethods interface { Method(i int) *types.Func } -func packConstant(typeName string, value constant.Value) NamedConstValue { - result := NamedConstValue{Type: typeName, Value: "nil"} +func packConstant(typ types.Type, value constant.Value) NamedConstValue { + result := NamedConstValue{Type: typ.String(), Value: "nil"} if value == nil { return result } result.Value = value.String() + if basic, ok := typ.Underlying().(*types.Basic); ok { + switch basic.Kind() { + case types.Float32: + number, _ := constant.Float32Val(value) + result.Value = strconv.FormatFloat(float64(number), 'g', -1, 32) + case types.Float64, types.UntypedFloat: + number, _ := constant.Float64Val(value) + result.Value = strconv.FormatFloat(number, 'g', -1, 64) + } + } + if value.Kind() == constant.String { encoded := base64.StdEncoding.EncodeToString([]byte(constant.StringVal(value))) result.Bytes = &encoded diff --git a/usvm-go/src/main/go/regressions/semantics.go b/usvm-go/src/main/go/regressions/semantics.go index dea41872aa..553c66650d 100644 --- a/usvm-go/src/main/go/regressions/semantics.go +++ b/usvm-go/src/main/go/regressions/semantics.go @@ -351,3 +351,89 @@ func nilNamedMapRange() int { } return iterations } + +type namedNumber int64 +type namedByte uint8 +type namedBool bool + +func namedNegation() int64 { + value := namedNumber(7) + return int64(-value) +} + +func namedComplement() uint8 { + value := namedByte(15) + return uint8(^value) +} + +func namedBooleanNot() bool { + value := namedBool(true) + return bool(!value) +} + +func unsignedToFloat64() bool { + value := uint64(0xffffffffffffffff) + return float64(value) == 0x1p64 +} + +func unsignedToFloat32() bool { + value := uint32(0xffffffff) + return float32(value) == 0x1p32 +} + +func floatToInt8() int8 { + value := -123.75 + return int8(value) +} + +func floatToUint8() uint8 { + value := 200.75 + return uint8(value) +} + +func floatToInt16() int16 { + value := -12345.99999 + return int16(value) +} + +func floatToUint16() uint16 { + value := 60000.75 + return uint16(value) +} + +func symbolicNamedNegation(value namedNumber) namedNumber { + return -value +} + +func symbolicNamedComplement(value namedByte) namedByte { + return ^value +} + +func symbolicNamedIdentity(value namedNumber) namedNumber { + return value +} + +func namedInterfaceAssert() int64 { + var value any = namedNumber(7) + return int64(value.(namedNumber)) +} + +func nilScalarAssertion() bool { + var value any + _, ok := value.(namedNumber) + return ok +} + +func failedNamedAssertionZero() int64 { + var input any = int64(7) + value, _ := input.(namedNumber) + return int64(value) +} + +func symbolicNamedInterfaceRoundTrip(value namedNumber) namedNumber { + if value == 0 { + return 0 + } + var boxed any = value + return boxed.(namedNumber) +} diff --git a/usvm-go/src/main/go/regressions/semantics_test.go b/usvm-go/src/main/go/regressions/semantics_test.go index b7b1b261fe..a57765b404 100644 --- a/usvm-go/src/main/go/regressions/semantics_test.go +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -9,6 +9,18 @@ import ( func TestNativeOracle(t *testing.T) { cases := map[string]func() any{ + "namedNegation": func() any { return namedNegation() }, + "namedComplement": func() any { return namedComplement() }, + "namedBooleanNot": func() any { return namedBooleanNot() }, + "unsignedToFloat64": func() any { return unsignedToFloat64() }, + "unsignedToFloat32": func() any { return unsignedToFloat32() }, + "floatToInt8": func() any { return floatToInt8() }, + "floatToUint8": func() any { return floatToUint8() }, + "floatToInt16": func() any { return floatToInt16() }, + "floatToUint16": func() any { return floatToUint16() }, + "namedInterfaceAssert": func() any { return namedInterfaceAssert() }, + "nilScalarAssertion": func() any { return nilScalarAssertion() }, + "failedNamedAssertionZero": func() any { return failedNamedAssertionZero() }, "nilMapLookupComma": func() any { return nilMapLookupComma() }, "nilMapDelete": func() any { return nilMapDelete() }, "nilMapAssignment": func() any { return nilMapAssignment() }, @@ -105,6 +117,8 @@ func TestReplaySymbolicInputs(t *testing.T) { outputs[index] = symbolicBranch(input) case "symbolicSliceAlias": outputs[index] = symbolicSliceAlias(input) + case "symbolicNamedInterfaceRoundTrip": + outputs[index] = int(symbolicNamedInterfaceRoundTrip(namedNumber(input))) default: t.Fatal("Unknown replay method") } diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index 5028c177ab..0966995f08 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -2,6 +2,7 @@ package org.usvm import io.ksmt.expr.KBitVec32Value import io.ksmt.expr.KConst +import io.ksmt.expr.KFpRoundingMode import io.ksmt.utils.asExpr import org.jacodb.go.api.ArrayType import org.jacodb.go.api.BasicType @@ -310,11 +311,26 @@ class GoExprVisitor( } val operand = unboxNamedPrimitive(value, expr.operand.type) val targetSort = ctx.typeToSort(targetType) - val converted = if (operand.sort is UBvSort && targetSort is UBvSort) { - val signed = !(sourceType as BasicType).typeName.startsWith("uint") - bv(operand).mkNarrow(targetSort.sizeBits.toInt(), signed = signed) - } else { - ctx.mkPrimitiveCast(operand, targetSort) + val sourceSigned = sourceType is BasicType && !sourceType.typeName.startsWith("uint") + val converted = when { + operand.sort is UBvSort && targetSort is UBvSort -> { + bv(operand).mkNarrow(targetSort.sizeBits.toInt(), signed = sourceSigned) + } + operand.sort is UBvSort && targetSort is UFpSort -> { + ctx.mkBvToFpExpr(targetSort, ctx.fpRoundingModeSortDefaultValue(), bv(operand), signed = sourceSigned) + } + operand.sort is UFpSort && targetSort is UBvSort -> { + val targetSigned = !(targetType as BasicType).typeName.startsWith("uint") + ctx.mkFpToBvExpr( + ctx.mkFpRoundingModeExpr(KFpRoundingMode.RoundTowardZero), + operand.asExpr(operand.sort as UFpSort), + targetSort.sizeBits.toInt(), + isSigned = targetSigned + ) + } + else -> { + ctx.mkPrimitiveCast(operand, targetSort) + } } return tryBox(converted, expr.type) } @@ -344,7 +360,11 @@ class GoExprVisitor( } override fun visitGoMakeInterfaceExpr(expr: GoMakeInterfaceExpr): UExpr { - val value = box(expr.value.accept(this@GoExprVisitor), expr.value.type) + val type = expr.value.type + val operand = expr.value.accept(this) + val isNamedScalar = type is NamedType && ctx.typeToSort(type.underlying()) != ctx.addressSort + val payload = if (isNamedScalar) unboxNamedPrimitive(operand, type) else operand + val value = box(payload, type) scope.doWithState { scope.assert(memory.types.evalIsSubtype(value, expr.type)) ?: throw GoStepAbort() } @@ -632,6 +652,10 @@ class GoExprVisitor( val assertType = expr.assertType.let { if (it is TupleType) it.types[0] else it } val assertSort = ctx.typeToSort(assertType) + val underlyingSort = ctx.typeToSort(assertType.underlying()) + if (underlyingSort is UBvSort || underlyingSort is UFpSort || underlyingSort == ctx.boolSort) { + return scalarTypeAssertion(x.asExpr(ctx.addressSort), assertType, commaOk = expr.type is TupleType) + } val commaOk = expr.type is TupleType val tupleType = TupleType(listOf(assertType, GoBasicTypes.BOOL)) @@ -666,6 +690,37 @@ class GoExprVisitor( } } + private fun scalarTypeAssertion(reference: UHeapRef, type: GoType, commaOk: Boolean): UExpr { + val valueSort = ctx.typeToSort(type.underlying()) + val isNil = ctx.mkHeapRefEq(reference, ctx.nullRef) + val boxed = ctx.mkIte( + isNil, + trueBranch = { box(valueSort.sampleUValue(), type) }, + falseBranch = { reference } + ) + // Every non-nil interface value is boxed, independently of whether its dynamic type matches. + scope.assert(isBoxed(boxed)) ?: throw GoStepAbort() + val matches = scope.calcOnState { + ctx.mkAnd(ctx.mkNot(isNil), memory.types.evalIsSupertype(boxed, type)) + } + val value = if (type is NamedType) { + boxed + } else { + scope.calcOnState { unbox(boxed, valueSort) } + } + + if (!commaOk) { + scope.fork(matches, blockOnFalseState = { panic("type assertion failed") }) ?: throw GoStepAbort() + return value + } + + val zero = tryBox(valueSort.sampleUValue(), type) + val result = ctx.mkIte(matches, value.asExpr(value.sort), zero.asExpr(value.sort)) + return scope.calcOnState { + mkTuple(TupleType(listOf(type, GoBasicTypes.BOOL)), result, matches) + } + } + override fun visitGoExtractExpr(expr: GoExtractExpr): UExpr { val tuple = expr.instance.accept(this).asExpr(ctx.addressSort) @@ -689,9 +744,12 @@ class GoExprVisitor( } override fun visitGoParameter(expr: GoParameter): UExpr { - return scope.calcOnState { + val value = scope.calcOnState { memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), expr.index)) } + ensureNamedScalar(value, expr.type) + + return value } override fun visitGoConst(expr: GoConst): UExpr { @@ -910,24 +968,22 @@ class GoExprVisitor( } private fun visitGoUnaryExpr(expr: GoUnaryExpr): UExpr { - val x = expr.value.accept(this) - return when (expr) { - is GoUnArrowExpr -> { - throw UnsupportedUnaryOperationException("channel operations") - } - is GoUnXorExpr -> { - GoUnaryOperator.Complement(x) - } - is GoUnNotExpr, is GoUnSubExpr -> { - GoUnaryOperator.Neg(x) - } - is GoUnMulExpr -> { - deref(x, ctx.typeToSort(expr.type)) - } - else -> { - throw UnknownUnaryOperationException(expr.toString()) - } + val value = expr.value.accept(this) + if (expr is GoUnArrowExpr) { + throw UnsupportedUnaryOperationException("channel operations") + } + if (expr is GoUnMulExpr) { + return deref(value, ctx.typeToSort(expr.type)) } + + val operand = unboxNamedPrimitive(value, expr.value.type) + val result = when (expr) { + is GoUnXorExpr -> GoUnaryOperator.Complement(operand) + is GoUnNotExpr, is GoUnSubExpr -> GoUnaryOperator.Neg(operand) + else -> throw UnknownUnaryOperationException(expr.toString()) + } + + return tryBox(result, expr.type) } private fun visitIndexExpr(instance: GoValue, idx: GoValue): Pair> { @@ -1018,12 +1074,29 @@ class GoExprVisitor( } } + private fun ensureNamedScalar(value: UExpr, type: GoType) { + if (type !is NamedType || ctx.typeToSort(type.underlying()) == ctx.addressSort) { + return + } + + // Named numeric/boolean values cannot be nil; boxing tags are representation constraints. + val reference = value.asExpr(ctx.addressSort) + scope.assert(ctx.mkNot(ctx.mkHeapRefEq(reference, ctx.nullRef))) ?: throw GoStepAbort() + scope.assert(isBoxed(reference)) ?: throw GoStepAbort() + } + private fun unboxNamedPrimitive(expr: UExpr, type: GoType): UExpr { if (type !is NamedType) { return expr } - return unbox(expr.asExpr(ctx.addressSort), ctx.typeToSort(type.underlying())) + val sort = ctx.typeToSort(type.underlying()) + if (sort == ctx.addressSort) { + return unbox(expr.asExpr(ctx.addressSort), sort) + } + ensureNamedScalar(expr, type) + + return scope.calcOnState { unbox(expr.asExpr(ctx.addressSort), sort) } } private fun tryBox(expr: UExpr, targetType: GoType): UExpr { 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 481ec8dbcf..bb6fa5de8d 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -31,7 +31,9 @@ import org.usvm.GoContext import org.usvm.NULL_ADDRESS import org.usvm.UAddressSort import org.usvm.UBoolSort +import org.usvm.UBvSort import org.usvm.UExpr +import org.usvm.UFpSort import org.usvm.UHeapRef import org.usvm.USort import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapAnyKey @@ -86,8 +88,14 @@ class GoTestInterpreter( val inputModel = InputModel(inputValues) val argumentsAfter = List(method.parameters.size) { index -> val type = method.parameters[index].type as GoType - val original = model.read(URegisterStackLValue(typeToSort(type), index)) - outputScope.convertExpr(original, type) + val valueSort = typeToSort(type.underlying()) + if (valueSort is UBvSort || valueSort is UFpSort || valueSort == boolSort) { + // Scalar arguments are passed by value; local assignments cannot change the caller's value. + inputValues[index] + } else { + val original = model.read(URegisterStackLValue(typeToSort(type), index)) + outputScope.convertExpr(original, type) + } } return if (state.isExceptional) { @@ -378,7 +386,12 @@ class GoTestInterpreter( val type = memory.typeStreamOf(iface).first() val index = 0 - return GoInterfaceValue(type, convertExpr(memory.readField(iface, index, typeToSort(type)), type)) + val value = if (type is NamedType) { + convertExpr(iface, type) + } else { + convertExpr(memory.readField(iface, index, typeToSort(type)), type) + } + return GoInterfaceValue(type, value) } fun resolvePointer(pointer: UHeapRef, baseType: GoType): Any? = with(ctx) { diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt index cccb0f0b1f..e295128dcb 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt @@ -2,7 +2,9 @@ package org.usvm.samples.arithmetic import org.junit.jupiter.api.Test import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults class ArithmeticRegressionTest : GoNativeTestRunner() { @Test @@ -60,4 +62,103 @@ class ArithmeticRegressionTest : GoNativeTestRunner() { @Test @GoSample(method = "unsignedWidening", fixture = "regressions") fun unsignedWidening() = checkNative(method = "unsignedWidening") + + @Test + @GoSample(method = "namedNegation", fixture = "regressions") + fun namedNegation() = checkNative(method = "namedNegation") + + @Test + @GoSample(method = "namedComplement", fixture = "regressions") + fun namedComplement() = checkNative(method = "namedComplement") + + @Test + @GoSample(method = "namedBooleanNot", fixture = "regressions") + fun namedBooleanNot() = checkNative(method = "namedBooleanNot") + + @Test + @GoSample(method = "unsignedToFloat64", fixture = "regressions") + fun unsignedToFloat64() = checkNative(method = "unsignedToFloat64") + + @Test + @GoSample(method = "unsignedToFloat32", fixture = "regressions") + fun unsignedToFloat32() = checkNative(method = "unsignedToFloat32") + + @Test + @GoSample(method = "floatToInt8", fixture = "regressions") + fun floatToInt8() = checkNative(method = "floatToInt8") + + @Test + @GoSample(method = "floatToUint8", fixture = "regressions") + fun floatToUint8() = checkNative(method = "floatToUint8") + + @Test + @GoSample(method = "floatToInt16", fixture = "regressions") + fun floatToInt16() = checkNative(method = "floatToInt16") + + @Test + @GoSample(method = "floatToUint16", fixture = "regressions") + fun floatToUint16() = checkNative(method = "floatToUint16") + + @Test + @GoSample(method = "namedInterfaceAssert", fixture = "regressions") + fun namedInterfaceAssert() = checkNative(method = "namedInterfaceAssert") + + @Test + @GoSample(method = "nilScalarAssertion", fixture = "regressions") + fun nilScalarAssertion() = checkNative(method = "nilScalarAssertion") + + @Test + @GoSample(method = "failedNamedAssertionZero", fixture = "regressions") + fun failedNamedAssertionZero() = checkNative(method = "failedNamedAssertionZero") + + @Test + @GoSample(method = "symbolicNamedNegation", fixture = "regressions") + fun symbolicNamedNegation() { + checkDiscoveredProperties( + method = "symbolicNamedNegation", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { value: Number, r: GoResult -> r.long == -value.toLong() }, + ) + } + + @Test + @GoSample(method = "symbolicNamedComplement", fixture = "regressions") + fun symbolicNamedComplement() { + checkDiscoveredProperties( + method = "symbolicNamedComplement", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { value: UByte, r: GoResult -> r.isSuccess && r.value == value.inv() }, + ) + } + + @Test + @GoSample(method = "symbolicNamedIdentity", fixture = "regressions") + fun symbolicNamedIdentity() { + checkDiscoveredProperties( + method = "symbolicNamedIdentity", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { value: Number, r: GoResult -> r.long == value.toLong() }, + ) + } + + @Test + @GoSample(method = "symbolicNamedInterfaceRoundTrip", fixture = "regressions") + fun symbolicNamedInterfaceRoundTrip() { + checkDiscoveredProperties( + method = "symbolicNamedInterfaceRoundTrip", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { value: Number, r: GoResult -> value.toLong() == 0L && r.long == 0L }, + { value: Number, r: GoResult -> value.toLong() != 0L && r.long == value.toLong() }, + ) + + checkParameterMutations( + method = "symbolicNamedInterfaceRoundTrip", + { execution -> + execution.argumentsAfter == execution.arguments && + execution.result.long == (execution.arguments.single() as Number).toLong() + }, + ) + + replayInputs(methodName = "symbolicNamedInterfaceRoundTrip", expectedExecutions = 2) + } } From 6f89767b02428458a870daa3910e3ddeda41308b Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Fri, 9 Oct 2026 16:16:48 +0300 Subject: [PATCH 5/6] Fix Go value semantics and replay generated inputs --- usvm-go/README.md | 96 ++++---- usvm-go/build.gradle.kts | 14 +- usvm-go/src/main/go/examples/replay_test.go | 113 +++++++++ usvm-go/src/main/go/regressions/semantics.go | 107 +++++++++ .../src/main/go/regressions/semantics_test.go | 146 +++++++----- .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 225 +++++++++--------- .../src/main/kotlin/org/usvm/GoInstVisitor.kt | 3 +- .../org/usvm/interpreter/GoInterpreter.kt | 7 +- .../org/usvm/interpreter/GoTestInterpreter.kt | 160 +++++++++---- .../kotlin/org/usvm/memory/GoArrayView.kt | 12 +- .../src/main/kotlin/org/usvm/state/GoState.kt | 79 +++++- .../main/kotlin/org/usvm/state/GoStateData.kt | 13 +- .../main/kotlin/org/usvm/state/GoValueCopy.kt | 54 +++++ .../kotlin/org/usvm/state/GoValueShape.kt | 69 ++++++ .../main/kotlin/org/usvm/type/GoTypeSystem.kt | 36 +-- .../org/usvm/samples/GoExamplesReplayTest.kt | 114 +++++++++ .../org/usvm/samples/SampleCoverageTest.kt | 2 + .../collections/maps/SlowMapIterationTest.kt | 15 +- .../samples/pointers/PointerRegressionTest.kt | 23 ++ .../org/usvm/samples/types/InterfacesTest.kt | 3 + .../usvm/samples/types/ValueRegressionTest.kt | 47 ++++ .../samples/unsupported/GoUnsupportedTest.kt | 7 + 22 files changed, 1026 insertions(+), 319 deletions(-) create mode 100644 usvm-go/src/main/go/examples/replay_test.go create mode 100644 usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt create mode 100644 usvm-go/src/main/kotlin/org/usvm/state/GoValueShape.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointerRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/types/ValueRegressionTest.kt diff --git a/usvm-go/README.md b/usvm-go/README.md index 5c514a6aa2..6b13d40a2f 100644 --- a/usvm-go/README.md +++ b/usvm-go/README.md @@ -56,11 +56,17 @@ retain structured values and interface dynamic types; these snapshots do not pre object identity or the complete alias graph. Function parameters remain mocked: the `call` example checks that mock contract, rather than replaying an actual function body. -The thematic `*RegressionTest` classes retain comparisons against native Go for 66 -zero-argument scalar/panic cases. Branch, slice-alias and named-number/interface -checks also replay seven generated concrete inputs in a native Go executable. Native scalar comparisons currently use textual representations; native -panic comparisons check occurrence, while symbolic sample expectations may also check -the payload. Native replay does not yet support arbitrary collection/struct inputs. +The thematic `*RegressionTest` classes compare results with native Go for **80 +zero-argument scalar/panic cases**. Branch, slice-alias and named-number/interface +checks replay seven generated concrete inputs in a native Go executable. +`GoExamplesReplayTest` additionally replays generated array, slice, pointer and object +inputs against the original example functions, checking return values, panic occurrence +and argument snapshots after execution. The manual map test replays the original +`mapLoopLen` function in the same way. Replay supports these explicitly registered +methods; it does not reconstruct arbitrary alias graphs or function inputs. + +Native scalar comparisons use textual representations. Native panic comparisons check +occurrence, while symbolic sample expectations may also check the payload. The five slow examples remain manual and bounded: @@ -71,43 +77,38 @@ The five slow examples remain manual and bounded: The nonterminating `loopInfinite` example expects no completed executions within the analysis budget. The remaining manual examples and `panicRecoverComplex` permit partial instruction coverage but still enforce their semantic expectations. - -The stronger symbolic expectations expose unresolved input-model and semantic defects. -They are kept as failing tests, without disabled tests or expected-failure wrappers. -The current local run has **181 default tests: 138 passed, 43 failed**, plus -**5 manual tests: 3 passed, 2 failed**, with no skipped tests. Detekt on Go main/test -sources and the project-list check pass. Failures are grouped as follows: - -| Test package | Default failures | -| --- | ---: | -| `collections.slices` | 20 | -| `types` | 11 | -| `collections.maps` | 0 | -| `objects` | 4 | -| `strings` | 4 | -| `algorithms` | 2 | -| `arrays` | 1 | -| `pointers` | 1 | - -The manual failures are `canVisitAllRooms` and `mapLoopLen`. Counts are local-run observations, not a -count of independent bugs or an exhaustive list; budgeted symbolic exploration and -model materialization may affect which witnesses are collected. This draft integration -requires those failures to be resolved before merging. - -Nine previously failing default map tests now pass. New native regressions check -nil-map lookup, comma-ok, deletion, range and assignment panic with integer keys/values, -including lookup/deletion/range on named maps. A symbolic comma-ok regression requires -witnesses for nil maps, absent keys and present keys, with input-dependent values. -The two constant nil-map range tests permit partial instruction coverage because their -loop bodies are unreachable; they still require exactly one native-matching execution. - -All 34 arithmetic/named-number tests now pass. Sixteen new regressions cover -non-nil named scalar inputs, unary operators and argument snapshots, numeric -interface assertions/round trips, unsigned integer-to-float conversion, finite -representable float-to-integer truncation, and exact float-literal export. -Twelve compare constant results with native Go; four have symbolic expectations. -The interface round-trip test also replays two generated inputs and checks that -passing a numeric argument by value preserves its original snapshot. +`assertCreatureFailNoComma` also permits partial instruction coverage: its `Person` to +`Building` assertion always panics, so the following return is unreachable. Every +collected execution must still satisfy its panic expectation. + +The current local validation passes **201 default tests** and **5 manual tests**, with +no failures or skipped tests. Detekt on Go main/test sources, `validateProjectList` +and `git diff --check` pass. These counts describe a bounded local run, not an exhaustive +proof or a count of independent semantics supported. + +The reproduced defects are now covered by symbolic expectations and selected native +comparisons: + +- Nil-map lookup, comma-ok, deletion, range and assignment panic, including named maps. + Absent comma-ok lookups return zero. Integer keys/values have native regressions; a + symbolic test requires nil, absent-key and present-key witnesses. +- Named scalars, unary operators, interface assertions, argument snapshots, unsigned + integer-to-float conversion, finite representable float-to-integer truncation and + exact float-literal export. All 34 arithmetic/named-number tests pass. +- Input shapes and snapshots for arrays, structs, named values, interfaces, maps and + slices; valid representation tags are constraints rather than artificial Go panics. +- Array/struct copying on assignment, calls and interface boxing, including nested + structs. Native regressions check that changing a copy preserves the original. +- Pointer conversions preserve nil and a shared pointee. Native regressions check + aliasing in both conversion directions and round-trip equality. +- Comma-ok assertions produce composite zero values; failed non-comma assertions panic. + Interface calls explore admissible concrete receivers and typed nil pointer panics. + Pointers to interfaces do not acquire the interface's methods. +- Model resolution refines oversized collection witnesses within the same path constraints. + If no witness fits the materialization limit, resolution reports unsupported rather + than truncating the value and presenting it as a successful concrete result. +- Slice bounds use direct comparisons without overflowing `limit + 1`. The map-iteration + expectation follows the source's zero-initialized keys, independently checked by replay. `generateGoImports` can export the import examples for investigation. The original import/standard-library exploratory factories depended on manually prepared dumps; @@ -135,12 +136,15 @@ support. In particular: validated; such results may depend on the Go target. - Collection sizes use a nonnegative BV32 domain; symbolic sizes are restricted to that domain. Input slices currently model capacity as length, and materialized - array/slice/string models are capped at 10,000 elements. + array/slice/string/map models must fit 10,000 elements for concrete resolution. + Oversized models are re-solved within the same path; paths requiring larger values + or exceeding the refinement budget are reported as unsupported. - Unknown external calls and function parameters retain the prototype's mocking behavior. `GoFunctionReference` identifies a function input that cannot be replayed. -- General array/struct value-copy behavior, pointer/interface equality, reference - map keys and zero values for composite/named map values need further semantic - validation. Nil-map regressions currently cover integer keys/values. +- Selected array/struct copies and scalar-pointer conversions have native validation. + General pointer/interface identity and equality, reference map keys, and zero values + for composite/named map values still need broader validation. Nil-map regressions + currently cover integer keys/values. - Package loading, standard-library integration and arbitrary repository workflows have not been validated end to end. Instruction coverage alone is not a semantic correctness check. diff --git a/usvm-go/build.gradle.kts b/usvm-go/build.gradle.kts index ef97d8419e..a8c131c39f 100644 --- a/usvm-go/build.gradle.kts +++ b/usvm-go/build.gradle.kts @@ -57,17 +57,27 @@ val compileGoReplay by tasks.registering(Exec::class) { commandLine("go", "test", "-c", "-o", generatedGo.get().file("native-replay.test").asFile.absolutePath, "./regressions") } +val compileGoExamplesReplay by tasks.registering(Exec::class) { + group = "verification" + description = "Compiles native replay for generated collection and pointer inputs." + workingDir(layout.projectDirectory.dir("src/main/go")) + environment("GOTOOLCHAIN", "go1.22.3") + inputs.files(goSources) + outputs.file(generatedGo.map { it.file("native-examples-replay.test") }) + commandLine("go", "test", "-c", "-o", generatedGo.get().file("native-examples-replay.test").asFile.absolutePath, "./examples") +} + tasks.withType().configureEach { systemProperty("usvm.go.generatedDir", generatedGo.get().asFile.absolutePath) timeout.set(Duration.ofMinutes(15)) } tasks.test { - dependsOn(generateGoIr, generateGoRegressions, generateGoOracle, compileGoReplay) + dependsOn(generateGoIr, generateGoRegressions, generateGoOracle, compileGoReplay, compileGoExamplesReplay) } tasks.named("manualTest") { - dependsOn(generateGoIr) + dependsOn(generateGoIr, compileGoExamplesReplay) } tasks.withType().configureEach { diff --git a/usvm-go/src/main/go/examples/replay_test.go b/usvm-go/src/main/go/examples/replay_test.go new file mode 100644 index 0000000000..6d0f27fa79 --- /dev/null +++ b/usvm-go/src/main/go/examples/replay_test.go @@ -0,0 +1,113 @@ +package examples + +import ( + "encoding/json" + "os" + "testing" +) + +type replayRequest struct { + Method string `json:"method"` + Inputs [][]json.RawMessage `json:"inputs"` +} + +type replayResult struct { + Value any `json:"value"` + IsPanic bool `json:"isPanic"` + ArgumentsAfter []any `json:"argumentsAfter"` +} + +func TestReplayExamples(t *testing.T) { + filename := os.Getenv("USVM_GO_REPLAY_FILE") + if filename == "" { + t.Skip("No symbolic inputs supplied") + } + data, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + var request replayRequest + if err := json.Unmarshal(data, &request); err != nil { + t.Fatal(err) + } + + results := make([]replayResult, len(request.Inputs)) + for index, arguments := range request.Inputs { + results[index] = replayExample(t, request.Method, arguments) + } + + data, err = json.Marshal(results) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filename, data, 0o600); err != nil { + t.Fatal(err) + } +} + +func replayExample(t *testing.T, method string, arguments []json.RawMessage) (result replayResult) { + t.Helper() + defer func() { + if recover() != nil { + result.IsPanic = true + } + }() + decode := func(index int, target any) { + if err := json.Unmarshal(arguments[index], target); err != nil { + t.Fatal(err) + } + } + + switch method { + case "sliceOverwrite", "sliceCopySimple": + var values []int + decode(0, &values) + // The frontend currently models input capacity as equal to length. + values = values[:len(values):len(values)] + result.ArgumentsAfter = []any{values} + if method == "sliceOverwrite" { + result.Value = sliceOverwrite(values) + } else { + result.Value = sliceCopySimple(values) + } + case "arrayIndex": + var values [3]int + var index int + decode(0, &values) + decode(1, &index) + result.ArgumentsAfter = []any{values, index} + result.Value = arrayIndex(values, index) + case "pointerChangeType": + var value *int + decode(0, &value) + result.ArgumentsAfter = []any{value} + result.Value = pointerChangeType(value) + case "(*usvm/examples.Object).Set": + var fields *struct { + Value int `json:"field0"` + } + var value int + decode(0, &fields) + decode(1, &value) + var object *Object + if fields != nil { + object = &Object{value: fields.Value} + } + result.ArgumentsAfter = []any{nil, value} + defer func() { + if object != nil { + result.ArgumentsAfter[0] = map[string]int{"field0": object.value} + } + }() + object.Set(value) + result.Value = "" + case "mapLoopLen": + var values map[int]int + decode(0, &values) + result.ArgumentsAfter = []any{values} + result.Value = mapLoopLen(values) + default: + t.Fatalf("Unknown replay method %s", method) + } + return result +} diff --git a/usvm-go/src/main/go/regressions/semantics.go b/usvm-go/src/main/go/regressions/semantics.go index 553c66650d..a361f48753 100644 --- a/usvm-go/src/main/go/regressions/semantics.go +++ b/usvm-go/src/main/go/regressions/semantics.go @@ -437,3 +437,110 @@ func symbolicNamedInterfaceRoundTrip(value namedNumber) namedNumber { var boxed any = value return boxed.(namedNumber) } + +func nilPointerConversion() bool { + var value *int64 + return (*namedNumber)(value) == nil +} + +func pointerConversionAlias() int64 { + value := int64(3) + alias := (*namedNumber)(&value) + *alias = 8 + value += 2 + return int64(*alias) + value +} + +func pointerConversionRoundTrip() bool { + value := int64(3) + pointer := &value + return (*int64)((*namedNumber)(pointer)) == pointer +} + +func namedPointerConversionAlias() int64 { + value := namedNumber(3) + alias := (*int64)(&value) + *alias = 8 + value += 2 + return *alias + int64(value) +} + +type valueRecord struct{ number int } + +type nestedValueRecord struct{ record valueRecord } + +func structValueCopy() int { + original := valueRecord{number: 3} + copied := original + copied.number = 8 + return original.number + copied.number +} + +func nestedStructValueCopy() int { + original := nestedValueRecord{record: valueRecord{number: 3}} + copied := original + copied.record.number = 8 + return original.record.number + copied.record.number +} + +func arrayValueCopy() int { + original := [2]int{3, 4} + copied := original + copied[0] = 8 + return original[0] + copied[0] +} + +func structArgumentCopy() int { + original := valueRecord{number: 3} + result := func(value valueRecord) int { + value.number = 8 + return value.number + }(original) + return original.number + result +} + +func arrayArgumentCopy() int { + original := [2]int{3, 4} + result := func(value [2]int) int { + value[0] = 8 + return value[0] + }(original) + return original[0] + result +} + +func interfaceStructCopy() int { + original := valueRecord{number: 3} + var boxed any = original + original.number = 8 + return boxed.(valueRecord).number + original.number +} + +func nilStructAssertionZero() int { + var boxed any + value, _ := boxed.(valueRecord) + return value.number +} + +func typedNilPointerAssertion() bool { + var value *valueRecord + var boxed any = value + result, ok := boxed.(*valueRecord) + return ok && result == nil && boxed != nil +} + +func nilStructAssertionOk() bool { + var boxed any + _, ok := boxed.(valueRecord) + return ok +} + +func pointerToInterfaceDoesNotImplement() bool { + var value *error + var boxed any = value + _, ok := boxed.(error) + return ok +} + +func oversizedResolvedSlice(_ int) []int { + return make([]int, 10001) +} diff --git a/usvm-go/src/main/go/regressions/semantics_test.go b/usvm-go/src/main/go/regressions/semantics_test.go index a57765b404..e9458313e3 100644 --- a/usvm-go/src/main/go/regressions/semantics_test.go +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -9,72 +9,86 @@ import ( func TestNativeOracle(t *testing.T) { cases := map[string]func() any{ - "namedNegation": func() any { return namedNegation() }, - "namedComplement": func() any { return namedComplement() }, - "namedBooleanNot": func() any { return namedBooleanNot() }, - "unsignedToFloat64": func() any { return unsignedToFloat64() }, - "unsignedToFloat32": func() any { return unsignedToFloat32() }, - "floatToInt8": func() any { return floatToInt8() }, - "floatToUint8": func() any { return floatToUint8() }, - "floatToInt16": func() any { return floatToInt16() }, - "floatToUint16": func() any { return floatToUint16() }, - "namedInterfaceAssert": func() any { return namedInterfaceAssert() }, - "nilScalarAssertion": func() any { return nilScalarAssertion() }, - "failedNamedAssertionZero": func() any { return failedNamedAssertionZero() }, - "nilMapLookupComma": func() any { return nilMapLookupComma() }, - "nilMapDelete": func() any { return nilMapDelete() }, - "nilMapAssignment": func() any { return nilMapAssignment() }, - "nilNamedMapLookup": func() any { return nilNamedMapLookup() }, - "nilNamedMapDelete": func() any { return nilNamedMapDelete() }, - "missingMapLookupCommaValue": func() any { return missingMapLookupCommaValue() }, - "nilMapRange": func() any { return nilMapRange() }, - "nilNamedMapRange": func() any { return nilNamedMapRange() }, - "nilMapLookup": func() any { return nilMapLookup() }, - "unsignedResultWidth": func() any { return unsignedResultWidth() }, - "shiftByBitWidth": func() any { return shiftByBitWidth() }, - "bitwiseComplement": func() any { return bitwiseComplement() }, - "bitwiseAndNot": func() any { return bitwiseAndNot() }, - "nativeIntWidth": func() any { return nativeIntWidth() }, - "sliceCapacity": func() any { return sliceCapacity() }, - "mapHintLength": func() any { return mapHintLength() }, - "mapInsertLength": func() any { return mapInsertLength() }, - "sliceAlias": func() any { return sliceAlias() }, - "stringEquality": func() any { return stringEquality() }, - "stringInequality": func() any { return stringInequality() }, - "stringLengthDifference": func() any { return stringLengthDifference() }, - "stringOrdering": func() any { return stringOrdering() }, - "stringPrefixOrdering": func() any { return stringPrefixOrdering() }, - "emptyStringEquality": func() any { return emptyStringEquality() }, - "stringConcatenation": func() any { return stringConcatenation() }, - "stringBytesCopy": func() any { return stringBytesCopy() }, - "bytesStringCopy": func() any { return bytesStringCopy() }, - "sliceOffsetAlias": func() any { return sliceOffsetAlias() }, - "sliceFullCapacity": func() any { return sliceFullCapacity() }, - "sliceCopyOffset": func() any { return sliceCopyOffset() }, - "sliceCopyString": func() any { return sliceCopyString() }, - "sliceAppendReuse": func() any { return sliceAppendReuse() }, - "sliceAppendAllocate": func() any { return sliceAppendAllocate() }, - "sliceAppendOffset": func() any { return sliceAppendOffset() }, - "sliceArrayPointerAlias": func() any { return sliceArrayPointerAlias() }, - "nilSliceLength": func() any { return nilSliceLength() }, - "nilSliceAppend": func() any { return nilSliceAppend() }, - "mapOverwriteLength": func() any { return mapOverwriteLength() }, - "mapDeleteLength": func() any { return mapDeleteLength() }, - "bitwiseAndOr": func() any { return bitwiseAndOr() }, - "oversizedShiftCount": func() any { return oversizedShiftCount() }, - "signedRightShift": func() any { return signedRightShift() }, - "unsignedRightShift": func() any { return unsignedRightShift() }, - "nativeIntOverflow": func() any { return nativeIntOverflow() }, - "unsignedWidening": func() any { return unsignedWidening() }, - "oversizedIndex": func() any { return oversizedIndex() }, - "negativeIndex": func() any { return negativeIndex() }, - "negativeShift": func() any { return negativeShift() }, - "divideByZero": func() any { return divideByZero() }, - "remainderByZero": func() any { return remainderByZero() }, - "utf8StringLength": func() any { return utf8StringLength() }, - "unsignedSliceLength": func() any { return unsignedSliceLength() }, - "narrowIndex": func() any { return narrowIndex() }, - "negativeSliceHigh": func() any { return negativeSliceHigh() }, + "namedNegation": func() any { return namedNegation() }, + "namedComplement": func() any { return namedComplement() }, + "namedBooleanNot": func() any { return namedBooleanNot() }, + "unsignedToFloat64": func() any { return unsignedToFloat64() }, + "unsignedToFloat32": func() any { return unsignedToFloat32() }, + "floatToInt8": func() any { return floatToInt8() }, + "floatToUint8": func() any { return floatToUint8() }, + "floatToInt16": func() any { return floatToInt16() }, + "floatToUint16": func() any { return floatToUint16() }, + "namedInterfaceAssert": func() any { return namedInterfaceAssert() }, + "nilScalarAssertion": func() any { return nilScalarAssertion() }, + "failedNamedAssertionZero": func() any { return failedNamedAssertionZero() }, + "nilMapLookupComma": func() any { return nilMapLookupComma() }, + "nilMapDelete": func() any { return nilMapDelete() }, + "nilMapAssignment": func() any { return nilMapAssignment() }, + "nilNamedMapLookup": func() any { return nilNamedMapLookup() }, + "nilNamedMapDelete": func() any { return nilNamedMapDelete() }, + "missingMapLookupCommaValue": func() any { return missingMapLookupCommaValue() }, + "nilMapRange": func() any { return nilMapRange() }, + "nilNamedMapRange": func() any { return nilNamedMapRange() }, + "nilMapLookup": func() any { return nilMapLookup() }, + "unsignedResultWidth": func() any { return unsignedResultWidth() }, + "shiftByBitWidth": func() any { return shiftByBitWidth() }, + "bitwiseComplement": func() any { return bitwiseComplement() }, + "bitwiseAndNot": func() any { return bitwiseAndNot() }, + "nativeIntWidth": func() any { return nativeIntWidth() }, + "sliceCapacity": func() any { return sliceCapacity() }, + "mapHintLength": func() any { return mapHintLength() }, + "mapInsertLength": func() any { return mapInsertLength() }, + "sliceAlias": func() any { return sliceAlias() }, + "stringEquality": func() any { return stringEquality() }, + "stringInequality": func() any { return stringInequality() }, + "stringLengthDifference": func() any { return stringLengthDifference() }, + "stringOrdering": func() any { return stringOrdering() }, + "stringPrefixOrdering": func() any { return stringPrefixOrdering() }, + "emptyStringEquality": func() any { return emptyStringEquality() }, + "stringConcatenation": func() any { return stringConcatenation() }, + "stringBytesCopy": func() any { return stringBytesCopy() }, + "bytesStringCopy": func() any { return bytesStringCopy() }, + "sliceOffsetAlias": func() any { return sliceOffsetAlias() }, + "sliceFullCapacity": func() any { return sliceFullCapacity() }, + "sliceCopyOffset": func() any { return sliceCopyOffset() }, + "sliceCopyString": func() any { return sliceCopyString() }, + "sliceAppendReuse": func() any { return sliceAppendReuse() }, + "sliceAppendAllocate": func() any { return sliceAppendAllocate() }, + "sliceAppendOffset": func() any { return sliceAppendOffset() }, + "sliceArrayPointerAlias": func() any { return sliceArrayPointerAlias() }, + "nilSliceLength": func() any { return nilSliceLength() }, + "nilSliceAppend": func() any { return nilSliceAppend() }, + "mapOverwriteLength": func() any { return mapOverwriteLength() }, + "mapDeleteLength": func() any { return mapDeleteLength() }, + "bitwiseAndOr": func() any { return bitwiseAndOr() }, + "oversizedShiftCount": func() any { return oversizedShiftCount() }, + "signedRightShift": func() any { return signedRightShift() }, + "unsignedRightShift": func() any { return unsignedRightShift() }, + "nativeIntOverflow": func() any { return nativeIntOverflow() }, + "unsignedWidening": func() any { return unsignedWidening() }, + "oversizedIndex": func() any { return oversizedIndex() }, + "negativeIndex": func() any { return negativeIndex() }, + "negativeShift": func() any { return negativeShift() }, + "divideByZero": func() any { return divideByZero() }, + "remainderByZero": func() any { return remainderByZero() }, + "utf8StringLength": func() any { return utf8StringLength() }, + "unsignedSliceLength": func() any { return unsignedSliceLength() }, + "narrowIndex": func() any { return narrowIndex() }, + "negativeSliceHigh": func() any { return negativeSliceHigh() }, + "nilPointerConversion": func() any { return nilPointerConversion() }, + "pointerConversionAlias": func() any { return pointerConversionAlias() }, + "pointerConversionRoundTrip": func() any { return pointerConversionRoundTrip() }, + "namedPointerConversionAlias": func() any { return namedPointerConversionAlias() }, + "structValueCopy": func() any { return structValueCopy() }, + "nestedStructValueCopy": func() any { return nestedStructValueCopy() }, + "arrayValueCopy": func() any { return arrayValueCopy() }, + "structArgumentCopy": func() any { return structArgumentCopy() }, + "arrayArgumentCopy": func() any { return arrayArgumentCopy() }, + "interfaceStructCopy": func() any { return interfaceStructCopy() }, + "nilStructAssertionZero": func() any { return nilStructAssertionZero() }, + "typedNilPointerAssertion": func() any { return typedNilPointerAssertion() }, + "nilStructAssertionOk": func() any { return nilStructAssertionOk() }, + "pointerToInterfaceDoesNotImplement": func() any { return pointerToInterfaceDoesNotImplement() }, } results := make(map[string]string, len(cases)) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index 0966995f08..6fce001a23 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -81,6 +81,7 @@ 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 @@ -130,6 +131,8 @@ 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.state.copyValue +import org.usvm.state.valueShape import org.usvm.statistics.ApplicationGraph import org.usvm.type.GoBasicTypes import org.usvm.type.underlying @@ -163,17 +166,10 @@ class GoExprVisitor( 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 GoStepAbort() - memory.typeStreamOf(instance).first() - } - program.findMethod(expr.location, "(${type.typeName}).${checkNotNull(expr.callee).name}") - } + if (expr.callee != null) return callInterface(expr, func) + val args = expr.args + val method = when { func is GoFunction -> { program.findMethod(expr.location, func.metName) } @@ -199,6 +195,10 @@ class GoExprVisitor( val parameters = args.map { it.accept(this) }.toTypedArray() val call = GoCall(method, applicationGraph.entryPoints(method).first()) + parameters.forEachIndexed { index, value -> + val parameterType = method.parameters[index].type as GoType + scope.assert(scope.calcOnState { valueShape(value, parameterType) }) ?: throw GoStepAbort() + } ctx.setMethodInfo(method, parameters) scope.doWithState { @@ -207,6 +207,63 @@ class GoExprVisitor( return ctx.noValue } + private fun callInterface(expr: GoCallExpr, receiver: GoValue): UExpr { + val instance = receiver.accept(this).asExpr(ctx.addressSort) + checkNotNull(instance) ?: throw GoStepAbort() + val args = expr.args.map { it.accept(this) } + val name = checkNotNull(expr.callee).name + val typeSystem = ctx.typeSystem() + val types = program.types.values.distinct().filter { + typeSystem.isInstantiable(it) && typeSystem.isSupertype(receiver.type, it) + } + val cases = mutableListOf, org.usvm.state.GoState.() -> Unit>>() + + for (type in types) { + val typeMatches = scope.calcOnState { + ctx.mkAnd(memory.types.evalIsSubtype(instance, type), memory.types.evalIsSupertype(instance, type)) + } + if (scope.checkSat(typeMatches) == null) continue + + val methods = program.findPackage(expr.location.method.packageName).methods + val receiverName = if (type is PointerType) "*${type.baseType.typeName}" else type.typeName + val direct = methods.firstOrNull { it.metName == "($receiverName).$name" } + val pointerType = type as? PointerType + val method = direct ?: methods.firstOrNull { it.metName == "(${pointerType?.baseType?.typeName}).$name" } + ?: return unsupportedExpr("interface receiver method ${type.typeName}.$name") + val payload = if (type is NamedType) { + instance + } else { + scope.calcOnState { unbox(instance, ctx.typeToSort(type)) } + } + val fallback = direct == null && pointerType != null + val isNil = ctx.mkHeapRefEq(payload.asExpr(ctx.addressSort), ctx.nullRef) + if (fallback) { + cases += ctx.mkAnd(typeMatches, isNil) to { panic("nil pointer receiver") } + } + val value = if (fallback) { + scope.calcOnState { deref(payload.asExpr(ctx.addressSort), ctx.addressSort) } + } else { + payload + } + val shape = scope.calcOnState { valueShape(value, method.parameters.first().type as GoType) } + val receiverShape = scope.calcOnState { valueShape(payload, type) } + val argumentShapes = args.mapIndexed { index, argument -> + scope.calcOnState { valueShape(argument, method.parameters[index + 1].type as GoType) } + } + val condition = ctx.mkAnd( + listOf(typeMatches, shape, receiverShape, if (fallback) ctx.mkNot(isNil) else ctx.trueExpr) + + argumentShapes + ) + cases += condition to { + ctx.setMethodInfo(method, (listOf(value) + args).toTypedArray()) + addCall(GoCall(method, applicationGraph.entryPoints(method).first()), currentStatement) + } + } + + scope.forkMulti(cases) + return ctx.noValue + } + override fun visitGoAllocExpr(expr: GoAllocExpr): UExpr { return mkPointer(expr.type) } @@ -362,9 +419,8 @@ class GoExprVisitor( override fun visitGoMakeInterfaceExpr(expr: GoMakeInterfaceExpr): UExpr { val type = expr.value.type val operand = expr.value.accept(this) - val isNamedScalar = type is NamedType && ctx.typeToSort(type.underlying()) != ctx.addressSort - val payload = if (isNamedScalar) unboxNamedPrimitive(operand, type) else operand - val value = box(payload, type) + val payload = if (type is NamedType) unboxNamedPrimitive(operand, type) else operand + val value = scope.calcOnState { box(copyValue(payload, type.underlying()), type) } scope.doWithState { scope.assert(memory.types.evalIsSubtype(value, expr.type)) ?: throw GoStepAbort() } @@ -398,7 +454,7 @@ class GoExprVisitor( checkLength(length) ?: throw GoStepAbort() checkLength(capacity) ?: throw GoStepAbort() - checkIndexOutOfBounds(length, ctx.mkSizeAddExpr(capacity, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() + checkSliceBound(length, capacity) ?: throw GoStepAbort() return scope.calcOnState { val type = expr.type.underlying() @@ -427,9 +483,9 @@ class GoExprVisitor( checkNegativeIndex(low) ?: throw GoStepAbort() checkNegativeIndex(high) ?: throw GoStepAbort() checkNegativeIndex(limit) ?: throw GoStepAbort() - checkIndexOutOfBounds(low, ctx.mkSizeAddExpr(high, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() - checkIndexOutOfBounds(high, ctx.mkSizeAddExpr(limit, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() - checkIndexOutOfBounds(limit, ctx.mkSizeAddExpr(view.capacity, ctx.mkSizeExpr(1))) ?: throw GoStepAbort() + checkSliceBound(low, high) ?: throw GoStepAbort() + checkSliceBound(high, limit) ?: throw GoStepAbort() + checkSliceBound(limit, view.capacity) ?: throw GoStepAbort() val result = scope.calcOnState { val reference = memory.allocateGoArray(expr.type.underlying(), ctx.sizeSort, count) @@ -473,8 +529,10 @@ class GoExprVisitor( } val pointer = expr.instance.accept(this).asExpr(ctx.addressSort) - val struct = deref(pointer, ctx.addressSort) - checkNotNull(struct) ?: throw GoStepAbort() + val baseType = (expr.instance.type.underlying() as PointerType).baseType + val value = deref(pointer, ctx.addressSort) + scope.assert(scope.calcOnState { valueShape(value, baseType) }) ?: throw GoStepAbort() + val struct = unboxNamedRef(value, baseType) val fieldType = (expr.type as PointerType).baseType val fieldLValue = UFieldLValue(ctx.typeToSort(fieldType), struct, expr.field) @@ -482,8 +540,9 @@ class GoExprVisitor( } override fun visitGoFieldExpr(expr: GoFieldExpr): UExpr { - val struct = expr.instance.accept(this).asExpr(ctx.addressSort) - checkNotNull(struct) ?: throw GoStepAbort() + val value = expr.instance.accept(this).asExpr(ctx.addressSort) + scope.assert(scope.calcOnState { valueShape(value, expr.instance.type) }) ?: throw GoStepAbort() + val struct = unboxNamedRef(value, expr.instance.type) return scope.calcOnState { memory.readField(struct, expr.field, ctx.typeToSort(expr.type)) } @@ -648,77 +707,32 @@ class GoExprVisitor( } 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 underlyingSort = ctx.typeToSort(assertType.underlying()) - if (underlyingSort is UBvSort || underlyingSort is UFpSort || underlyingSort == ctx.boolSort) { - return scalarTypeAssertion(x.asExpr(ctx.addressSort), assertType, commaOk = expr.type is TupleType) - } - + val reference = expr.instance.accept(this).asExpr(ctx.addressSort) + val type = expr.assertType.let { if (it is TupleType) it.types[0] else it } 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 GoStepAbort() - 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) - } - } - } - - private fun scalarTypeAssertion(reference: UHeapRef, type: GoType, commaOk: Boolean): UExpr { - val valueSort = ctx.typeToSort(type.underlying()) + val zero = scope.calcOnState { sampleValue(type) } val isNil = ctx.mkHeapRefEq(reference, ctx.nullRef) - val boxed = ctx.mkIte( - isNil, - trueBranch = { box(valueSort.sampleUValue(), type) }, - falseBranch = { reference } - ) - // Every non-nil interface value is boxed, independently of whether its dynamic type matches. + val boxed = ctx.mkIte(isNil, trueBranch = { box(zero, type) }, falseBranch = { reference }) scope.assert(isBoxed(boxed)) ?: throw GoStepAbort() + val matches = scope.calcOnState { ctx.mkAnd(ctx.mkNot(isNil), memory.types.evalIsSupertype(boxed, type)) } - val value = if (type is NamedType) { + val value = if (type is NamedType || type.underlying() is InterfaceType) { boxed } else { - scope.calcOnState { unbox(boxed, valueSort) } + scope.calcOnState { unbox(boxed, ctx.typeToSort(type)) } } if (!commaOk) { scope.fork(matches, blockOnFalseState = { panic("type assertion failed") }) ?: throw GoStepAbort() + scope.assert(scope.calcOnState { valueShape(value, type) }) ?: throw GoStepAbort() return value } - val zero = tryBox(valueSort.sampleUValue(), type) val result = ctx.mkIte(matches, value.asExpr(value.sort), zero.asExpr(value.sort)) - return scope.calcOnState { - mkTuple(TupleType(listOf(type, GoBasicTypes.BOOL)), result, matches) - } + scope.assert(scope.calcOnState { valueShape(result, type) }) ?: throw GoStepAbort() + return scope.calcOnState { mkTuple(TupleType(listOf(type, GoBasicTypes.BOOL)), result, matches) } } override fun visitGoExtractExpr(expr: GoExtractExpr): UExpr { @@ -824,7 +838,14 @@ class GoExprVisitor( return tryBox(mkFp(value.value, fp64Sort), value.type) } - override fun visitGoNullConstant(value: GoNullConstant): UExpr = ctx.nullRef + override fun visitGoNullConstant(value: GoNullConstant): UExpr { + val type = value.type.underlying() + return if (type is ArrayType || type is org.jacodb.go.api.StructType) { + scope.calcOnState { sampleValue(value.type) } + } else { + ctx.nullRef + } + } override fun visitGoStringConstant(value: GoStringConstant): UExpr { return scope.calcOnState { @@ -839,21 +860,16 @@ class GoExprVisitor( } fun unboxNamedRef(expr: UHeapRef, type: GoType): UHeapRef { - if (type !is NamedType) { + if (type !is NamedType || type.underlying() is InterfaceType) { return expr } - if (type.underlying() !is MapType) { - return unbox(expr, ctx.typeToSort(type.underlying())).asExpr(ctx.addressSort) - } - - // Named maps may be nil, while every non-nil reference must carry the boxing tag. + scope.assert(scope.calcOnState { valueShape(expr, type) }) ?: throw GoStepAbort() val boxed = ctx.mkIte( ctx.mkHeapRefEq(expr, ctx.nullRef), trueBranch = { box(ctx.nullRef, type) }, falseBranch = { expr } ) - scope.assert(isBoxed(boxed)) ?: throw GoStepAbort() return scope.calcOnState { unbox(boxed, ctx.addressSort).asExpr(ctx.addressSort) } } @@ -881,8 +897,9 @@ class GoExprVisitor( val isEquality = expr is GoEqlExpr || expr is GoNeqExpr val hasNilOperand = expr.lhv is GoNullConstant || expr.rhv is GoNullConstant val bothReferences = lhv.sort == ctx.addressSort && rhv.sort == ctx.addressSort - if (isEquality && hasNilOperand && bothReferences) { - val equal = ctx.mkHeapRefEq(lhv.asExpr(ctx.addressSort), rhv.asExpr(ctx.addressSort)) + val pointerOperands = expr.lhv.type.underlying() is PointerType || expr.rhv.type.underlying() is PointerType + if (isEquality && bothReferences && (hasNilOperand || pointerOperands)) { + val equal = referenceEquals(lhv.asExpr(ctx.addressSort), rhv.asExpr(ctx.addressSort)) return if (expr is GoEqlExpr) equal else ctx.mkNot(equal) } @@ -1053,7 +1070,7 @@ class GoExprVisitor( private fun deref(expr: UExpr, sort: Sort): UExpr = with(ctx) { val pointer = expr.asExpr(addressSort) - checkIsPointer(pointer) ?: throw GoStepAbort() + scope.assert(ctx.mkOr(ctx.mkHeapRefEq(pointer, nullRef), isPointer(pointer))) ?: throw GoStepAbort() checkNotNull(pointer) ?: throw GoStepAbort() return scope.calcOnState { deref(pointer, sort).asExpr(sort) @@ -1086,13 +1103,13 @@ class GoExprVisitor( } private fun unboxNamedPrimitive(expr: UExpr, type: GoType): UExpr { - if (type !is NamedType) { + if (type !is NamedType || type.underlying() is InterfaceType) { return expr } val sort = ctx.typeToSort(type.underlying()) if (sort == ctx.addressSort) { - return unbox(expr.asExpr(ctx.addressSort), sort) + return unboxNamedRef(expr.asExpr(ctx.addressSort), type) } ensureNamedScalar(expr, type) @@ -1100,19 +1117,22 @@ class GoExprVisitor( } private fun tryBox(expr: UExpr, targetType: GoType): UExpr { - return if (targetType is NamedType) box(expr, targetType) else expr + return if (targetType is NamedType && targetType.underlying() !is InterfaceType) 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 referenceEquals(left: UHeapRef, right: UHeapRef): UExpr = scope.calcOnState { + ctx.mkHeapRefEq(canonicalPointer(left), canonicalPointer(right)) } + private fun checkSliceBound(bound: UExpr, limit: UExpr): Unit? = + scope.fork(ctx.mkSizeLeExpr(bound, limit), blockOnFalseState = { + panic("slice bounds out of range") + }) + private fun checkNegativeIndex(value: UExpr): Unit? = with(ctx) { scope.fork(mkSizeGeExpr(value, mkSizeExpr(0)), blockOnFalseState = { panic("negative index") @@ -1132,10 +1152,6 @@ class GoExprVisitor( }) } - 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") }) @@ -1388,17 +1404,8 @@ class GoExprVisitor( } 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) } - } - ) + val sourceType = (baseType.underlying() as PointerType).baseType + scope.calcOnState { convertPointer(value.asExpr(ctx.addressSort), sourceType, targetType.baseType) } } else -> { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt index 417054a5e8..4a51c5d9e9 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -33,6 +33,7 @@ import org.usvm.interpreter.GoStepAbort import org.usvm.interpreter.GoStepScope import org.usvm.memory.URegisterStackLValue import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.state.copyValue import org.usvm.statistics.ApplicationGraph import org.usvm.type.underlying @@ -130,7 +131,7 @@ class GoInstVisitor( } val rvalue = inst.rhv.accept(exprVisitor) scope.doWithState { - store(pointer, rvalue) + store(pointer, copyValue(rvalue, inst.rhv.type)) } return next(inst) diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt index 04755a5e76..261c64ece8 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt @@ -40,10 +40,6 @@ class GoInterpreter( 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) @@ -73,6 +69,9 @@ class GoInterpreter( previousEntrypoint = m.blocks[0].instructions[0] } + val model = (solver().check(state.pathConstraints) as USatResult).model + state.models = listOf(model) + return state } 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 bb6fa5de8d..2443954134 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -6,6 +6,7 @@ import io.ksmt.expr.KBitVec64Value import io.ksmt.expr.KBitVec8Value import io.ksmt.expr.KFp32Value import io.ksmt.expr.KFp64Value +import io.ksmt.expr.KIteExpr import io.ksmt.sort.KBoolSort import io.ksmt.sort.KBv16Sort import io.ksmt.sort.KBv32Sort @@ -28,13 +29,14 @@ 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.UBoolExpr import org.usvm.UBoolSort import org.usvm.UBvSort import org.usvm.UExpr import org.usvm.UFpSort import org.usvm.UHeapRef +import org.usvm.USizeSort import org.usvm.USort import org.usvm.api.collection.ObjectMapCollectionApi.symbolicObjectMapAnyKey import org.usvm.api.readField @@ -46,19 +48,23 @@ 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.collections.immutable.internal.MutabilityOwnership +import org.usvm.constraints.UPathConstraints import org.usvm.isTrue import org.usvm.memory.ULValue -import org.usvm.memory.URegisterStackLValue +import org.usvm.memory.UMemory import org.usvm.memory.UWritableMemory import org.usvm.memory.arrayView import org.usvm.memory.key.USizeExprKeyInfo import org.usvm.memory.readGoArrayIndex import org.usvm.mkSizeAddExpr import org.usvm.mkSizeExpr +import org.usvm.mkSizeGeExpr +import org.usvm.mkSizeLeExpr import org.usvm.model.UModelBase import org.usvm.sampleUValue import org.usvm.sizeSort +import org.usvm.solver.USatResult import org.usvm.state.GoMethodResult import org.usvm.state.GoState import org.usvm.type.GoBasicTypes @@ -73,16 +79,46 @@ import kotlin.random.nextULong class GoTestInterpreter( private val ctx: GoContext, ) { - fun resolve(state: GoState, method: GoMethod): ProgramExecutionResult = with(ctx) { - val model = state.models.first() + fun resolve(state: GoState, method: GoMethod): ProgramExecutionResult { + var model = state.models.first() + val boundedConstraints by lazy { state.clone().pathConstraints } + val bounds = mutableSetOf() + + repeat(MAX_MODEL_REFINEMENTS) { + try { + return resolveWithModel(state, method, model) + } catch (limit: MaterializationBoundException) { + if (!bounds.add(limit.bound)) { + throw UnsupportedOperationException("Cannot materialize Go model within size limit") + } + model = refineModel(boundedConstraints, limit.bound) + } + } + throw UnsupportedOperationException("Go model size refinement limit exceeded") + } - val inputScope = MemoryScope(ctx, state, model, model) + private fun refineModel(constraints: UPathConstraints, bound: UBoolExpr): UModelBase { + constraints += bound + val result = ctx.solver().check(constraints) + return (result as? USatResult)?.model ?: throw UnsupportedOperationException( + "Cannot materialize Go model within size limit: $result" + ) + } + + private fun resolveWithModel( + state: GoState, + method: GoMethod, + model: UModelBase, + ): ProgramExecutionResult = with(ctx) { + // Keep input reads symbolic so size refinement can re-solve them without using the mutated heap. + val inputMemory = UMemory(ctx, MutabilityOwnership(), state.pathConstraints.typeConstraints) + val inputScope = MemoryScope(ctx, state, model, inputMemory) 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)) + val expr = mkRegisterReading(idx, sort) inputScope.convertExpr(expr, type) } val inputModel = InputModel(inputValues) @@ -93,7 +129,7 @@ class GoTestInterpreter( // Scalar arguments are passed by value; local assignments cannot change the caller's value. inputValues[index] } else { - val original = model.read(URegisterStackLValue(typeToSort(type), index)) + val original = mkRegisterReading(index, typeToSort(type)) outputScope.convertExpr(original, type) } } @@ -162,7 +198,7 @@ class GoTestInterpreter( private fun resolveReference(reference: UHeapRef, baseType: GoType): Any? { val type = baseType.underlying() - if (baseType is NamedType) return resolveBoxed(reference, type) + if (baseType is NamedType && type !is InterfaceType) return resolveBoxed(reference, type) return when (type) { GoBasicTypes.STRING -> { resolveString(reference, type) @@ -219,14 +255,26 @@ class GoTestInterpreter( fun resolveSize(expr: UExpr) = (model.eval(expr) as KBitVec32Value).numberValue + private fun resolveLength(expr: UExpr): Int { + val length = resolveSize(expr) + if (length !in 0..MAX_ARRAY_LENGTH) { + val bound = ctx.mkAnd( + ctx.mkSizeGeExpr(expr, ctx.mkSizeExpr(0)), + ctx.mkSizeLeExpr(expr, ctx.mkSizeExpr(MAX_ARRAY_LENGTH)) + ) + throw MaterializationBoundException(bound) + } + return length + } + fun resolveString(string: UHeapRef, arrayType: GoType): String = with(ctx) { - if (string == mkConcreteHeapRef(NULL_ADDRESS) || string == nullRef) { + if (model.eval(string) == model.nullRef) { return "" } - val view = state.arrayView(string, arrayType) + val view = state.arrayView(string, arrayType, sourceMemory = memory) val lengthUExpr = view.length - val length = clipArrayLength(resolveSize(lengthUExpr)) + val length = resolveLength(lengthUExpr) val buffer = ByteBuffer.allocate(length * Byte.SIZE_BYTES) for (i in 0..? = with(ctx) { - if (array == mkConcreteHeapRef(NULL_ADDRESS) || array == nullRef) { + if (model.eval(array) == model.nullRef) { return null } - val view = state.arrayView(array, arrayType) - val length = clipArrayLength(len.toInt()) + val view = state.arrayView(array, arrayType, sourceMemory = memory) + if (len !in 0..MAX_ARRAY_LENGTH.toLong()) { + throw UnsupportedOperationException("Cannot materialize Go array of length $len") + } + val length = len.toInt() val sort = typeToSort(elementType) return List(length) { idx -> val element = memory.readGoArrayIndex( @@ -265,13 +316,13 @@ class GoTestInterpreter( } fun resolveSlice(slice: UHeapRef, sliceType: GoType, elementType: GoType): List? = with(ctx) { - if (slice == mkConcreteHeapRef(NULL_ADDRESS) || slice == nullRef) { + if (model.eval(slice) == model.nullRef) { return null } - val view = state.arrayView(slice, sliceType) + val view = state.arrayView(slice, sliceType, sourceMemory = memory) val lengthUExpr = view.length - val length = clipArrayLength(resolveSize(lengthUExpr)) + val length = resolveLength(lengthUExpr) val sort = typeToSort(elementType) return List(length) { idx -> val offset = view.offset @@ -290,7 +341,7 @@ class GoTestInterpreter( fun resolveMap(map: UHeapRef, mapType: GoType, keyType: GoType, valueType: GoType): Map? = with( ctx ) { - if (map == mkConcreteHeapRef(NULL_ADDRESS) || map == nullRef) { + if (model.eval(map) == model.nullRef) { return null } @@ -301,7 +352,7 @@ class GoTestInterpreter( val addToMap: (MutableMap, Set>) -> Unit = { m, s -> m.putAll( - s.associate { entry -> + s.mapNotNull { entry -> val key = when (entry) { is URefSetEntryLValue<*> -> { entry.setElement @@ -314,6 +365,13 @@ class GoTestInterpreter( } } + val membership = if (isRefSet) { + URefSetEntryLValue(map, key.asExpr(addressSort), mapType) + } else { + USetEntryLValue(keySort, map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) + } + if (!resolveBool(memory.read(membership))) return@mapNotNull null + val lvalue = if (isRefSet) { URefMapEntryLValue(valueSort, map, key.asExpr(addressSort), mapType) } else { @@ -321,22 +379,22 @@ class GoTestInterpreter( } val value = memory.read(lvalue) convertExpr(key, keyType) to convertExpr(value, valueType) - } + }.toMap() ) } - val getEntries: (UHeapRef) -> Set> = { + val getEntries: (UWritableMemory, UHeapRef) -> Set> = { source, ref -> if (isRefSet) { - memory.refSetEntries(it, mapType) + source.refSetEntries(ref, mapType) } else { - memory.setEntries(it, mapType, keySort, USizeExprKeyInfo()) + source.setEntries(ref, mapType, keySort, USizeExprKeyInfo()) }.entries } - val length = clipArrayLength(resolveSize(memory.read(UMapLengthLValue(map, mapType, sizeSort)))) + val length = resolveLength(memory.read(UMapLengthLValue(map, mapType, sizeSort))) val result = mutableMapOf() - getEntries(map).also { addToMap(result, it) } - getEntries(model.eval(map)).also { addToMap(result, it) } + getEntries(memory, map).also { addToMap(result, it) } + getEntries(model, model.eval(map)).also { addToMap(result, it) } if (length > result.size) { val diff = length - result.size @@ -356,7 +414,7 @@ class GoTestInterpreter( } fun resolveTuple(tuple: UHeapRef, tupleType: TupleType): List? = with(ctx) { - if (tuple == mkConcreteHeapRef(NULL_ADDRESS) || tuple == nullRef) { + if (model.eval(tuple) == model.nullRef) { return null } @@ -367,7 +425,7 @@ class GoTestInterpreter( } fun resolveStruct(struct: UHeapRef, structType: StructType): Map? = with(ctx) { - if (struct == mkConcreteHeapRef(NULL_ADDRESS) || struct == nullRef) { + if (model.eval(struct) == model.nullRef) { return null } @@ -380,11 +438,15 @@ class GoTestInterpreter( } fun resolveInterface(iface: UHeapRef): Any? = with(ctx) { - if (iface == mkConcreteHeapRef(NULL_ADDRESS) || iface == nullRef) { + if (model.eval(iface) == model.nullRef) { return null } - val type = memory.typeStreamOf(iface).first() + val type = if (org.usvm.isAllocatedConcreteHeapRef(iface)) { + state.memory.typeStreamOf(iface).first() + } else { + model.typeStreamOf(model.eval(iface)).first() + } val index = 0 val value = if (type is NamedType) { convertExpr(iface, type) @@ -395,7 +457,15 @@ class GoTestInterpreter( } fun resolvePointer(pointer: UHeapRef, baseType: GoType): Any? = with(ctx) { - if (pointer == mkConcreteHeapRef(NULL_ADDRESS) || pointer == nullRef) { + if (pointer is KIteExpr) { + val branch = if (resolveBool(pointer.condition)) pointer.trueBranch else pointer.falseBranch + return resolvePointer(branch, baseType) + } + state.data.pointerConversions[pointer]?.let { conversion -> + return resolvePointer(conversion.source, conversion.sourceType) + } + + if (model.eval(pointer) == model.nullRef) { return null } @@ -414,7 +484,7 @@ class GoTestInterpreter( } fun resolveBoxed(value: UHeapRef, type: GoType): Any? = with(ctx) { - if (value == mkConcreteHeapRef(NULL_ADDRESS) || value == nullRef) { + if (model.eval(value) == model.nullRef) { return null } @@ -423,26 +493,12 @@ class GoTestInterpreter( } } - 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 + private companion object { + const val MAX_ARRAY_LENGTH = 10_000 + const val MAX_MODEL_REFINEMENTS = 32 } + + private class MaterializationBoundException(val bound: UBoolExpr) : RuntimeException() } sealed interface ProgramExecutionResult diff --git a/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt index 1e47b54d5a..cb27a01b79 100644 --- a/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt +++ b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt @@ -17,12 +17,16 @@ internal data class GoArrayView( val capacity: UExpr, ) -internal fun GoState.arrayView(reference: UHeapRef, type: GoType): GoArrayView { +internal fun GoState.arrayView( + reference: UHeapRef, + type: GoType, + sourceMemory: UReadOnlyMemory = memory, +): GoArrayView { data.arrayViews[reference]?.let { return it } if (reference is KIteExpr) { - val positive = arrayView(reference.trueBranch, type) - val negative = arrayView(reference.falseBranch, type) + val positive = arrayView(reference.trueBranch, type, sourceMemory) + val negative = arrayView(reference.falseBranch, type, sourceMemory) check(positive.storageType == negative.storageType) { "Array view storage types differ" } return GoArrayView( ctx.mkIte(reference.condition, positive.backing, negative.backing), @@ -37,7 +41,7 @@ internal fun GoState.arrayView(reference: UHeapRef, type: GoType): GoArrayView { val length = ctx.mkIte( ctx.mkHeapRefEq(reference, ctx.nullRef), trueBranch = { zero }, - falseBranch = { memory.readGoArrayLength(reference, type, ctx.sizeSort) }, + falseBranch = { sourceMemory.readGoArrayLength(reference, type, ctx.sizeSort) }, ) return GoArrayView(reference, type.arrayStorageType(), offset = zero, length = length, capacity = length) } 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 966a5594c8..19662f6e09 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -15,9 +15,14 @@ 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.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.SignatureType import org.jacodb.go.api.SliceType +import org.jacodb.go.api.StructType import org.usvm.GoCall import org.usvm.GoContext import org.usvm.GoTarget @@ -38,7 +43,6 @@ import org.usvm.constraints.UPathConstraints import org.usvm.memory.ULValue import org.usvm.memory.UMemory import org.usvm.memory.URegisterStackLValue -import org.usvm.memory.allocateGoArray import org.usvm.memory.allocateGoArrayInitialized import org.usvm.memory.writeGoArrayLength import org.usvm.merging.MutableMergeGuard @@ -230,7 +234,11 @@ class GoState( } parameters.forEachIndexed { i, parameter -> - when (val type = parameter.type) { + val parameterType = parameter.type as GoType + val value = memory.read(URegisterStackLValue(typeToSort(parameterType), i)) + pathConstraints += valueShape(value, parameterType) + + when (val type = parameterType.underlying()) { is ArrayType -> { val ref = memory.read(URegisterStackLValue(addressSort, i)).asExpr(addressSort) memory.writeGoArrayLength(ref, mkSizeExpr(type.len.toInt()), type, sizeSort) @@ -238,6 +246,12 @@ class GoState( } } + parameters.forEachIndexed { index, parameter -> + val type = parameter.type as GoType + val lvalue = URegisterStackLValue(typeToSort(type), index) + memory.write(lvalue, copyValue(memory.read(lvalue), type).asExpr(lvalue.sort), trueExpr) + } + freeVariables.forEachIndexed { i, variable -> val lvalue = URegisterStackLValue(variable.sort, i + freeVariableOffset(call.method)) memory.write(lvalue, variable.asExpr(variable.sort), trueExpr) @@ -254,6 +268,10 @@ class GoState( deref(pointer.falseBranch, sort).asExpr(sort) ) } + data.pointerConversions[pointer]?.let { conversion -> + val value = deref(conversion.source, ctx.typeToSort(conversion.sourceType)) + return reinterpretValue(value, conversion.sourceType, conversion.targetType) + } val target = data.pointerTargets[pointer] return if (target != null) memory.read(target) else memory.readField(pointer, 0, sort) } @@ -268,6 +286,11 @@ class GoState( storeGuarded(pointer.falseBranch, rvalue, ctx.mkAnd(guard, ctx.mkNot(pointer.condition))) return } + data.pointerConversions[pointer]?.let { conversion -> + val value = reinterpretValue(rvalue, conversion.targetType, conversion.sourceType) + storeGuarded(conversion.source, value, guard) + return + } val target = data.pointerTargets[pointer] if (target != null) { memory.write(target.withSort(rvalue.sort), rvalue.asExpr(rvalue.sort), guard) @@ -276,6 +299,37 @@ class GoState( } } + internal fun convertPointer(source: UHeapRef, sourceType: GoType, targetType: GoType): UHeapRef { + val pointer = memory.allocConcrete(PointerType(targetType)) + memory.writeField( + ref = pointer, + field = 1, + sort = ctx.bv32Sort, + value = ctx.mkBv(POINTER_FIELD, ctx.bv32Sort), + guard = ctx.trueExpr + ) + data.pointerConversions[pointer] = GoPointerConversion(source, sourceType, targetType) + return ctx.mkIte(ctx.mkHeapRefEq(source, ctx.nullRef), ctx.nullRef, pointer) + } + + internal fun canonicalPointer(reference: UHeapRef): UHeapRef = when (reference) { + is KIteExpr -> ctx.mkIte( + reference.condition, + canonicalPointer(reference.trueBranch), + canonicalPointer(reference.falseBranch), + ) + else -> data.pointerConversions[reference]?.let { canonicalPointer(it.source) } ?: reference + } + + private fun reinterpretValue(value: UExpr, sourceType: GoType, targetType: GoType): UExpr { + val payload = if (sourceType is NamedType) { + unbox(value.asExpr(ctx.addressSort), ctx.typeToSort(sourceType.underlying())) + } else { + value + } + return if (targetType is NamedType) box(payload, targetType) else payload + } + fun isPointer(pointer: UHeapRef): UExpr { val index = 1 val field = memory.readField(pointer, index, ctx.bv32Sort) @@ -319,7 +373,13 @@ class GoState( fun mkPointer(type: GoType, lvalue: ULValue<*, *>): UExpr { data.pointerTargets.entries.firstOrNull { it.value == lvalue }?.let { return it.key } val pointer = memory.allocConcrete(PointerType(type)) - memory.writeField(pointer, 1, ctx.bv32Sort, ctx.mkBv(POINTER_FIELD, ctx.bv32Sort), ctx.trueExpr) + memory.writeField( + ref = pointer, + field = 1, + sort = ctx.bv32Sort, + value = ctx.mkBv(POINTER_FIELD, ctx.bv32Sort), + guard = ctx.trueExpr + ) data.pointerTargets[pointer] = lvalue return pointer } @@ -343,21 +403,24 @@ class GoState( ) } - private fun sampleValue(type: GoType): UExpr = when (type) { + 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.allocateGoArrayInitialized(type, sort, ctx.sizeSort, contents) } - is SliceType -> { - memory.allocateGoArray(type, ctx.sizeSort, ctx.mkSizeExpr(0)) + is SliceType, is MapType, is PointerType, is InterfaceType, is SignatureType, is NullType -> { + ctx.nullRef + } + is StructType -> { + mkTuple(type, fields = type.fields.orEmpty().map(::sampleValue).toTypedArray()) } is BasicType -> { - ctx.typeToSort(type).sampleUValue() + if (type == GoBasicTypes.STRING) mkString("") else ctx.typeToSort(type).sampleUValue() } is NamedType -> { - box(sampleValue(type.underlyingType), type) + if (type.underlying() is InterfaceType) ctx.nullRef else box(sampleValue(type.underlyingType), type) } else -> { memory.allocConcrete(type) 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 ed5d572038..a9ef8a6a4b 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt @@ -1,6 +1,7 @@ package org.usvm.state import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoType import org.usvm.GoCall import org.usvm.UHeapRef import org.usvm.memory.GoArrayView @@ -19,6 +20,8 @@ class GoStateData( internal val pointerTargets: MutableMap> = hashMapOf() + internal val pointerConversions: MutableMap = hashMapOf() + private val deferredCalls: MutableMap> = hashMapOf() val flowStatus: GoFlowStatus @@ -32,6 +35,7 @@ class GoStateData( fun clone(): GoStateData = GoStateData(clonedFlowStack()).also { it.pointerTargets.putAll(pointerTargets) + it.pointerConversions.putAll(pointerConversions) it.arrayViews.putAll(arrayViews) for ((method, calls) in deferredCalls) { calls.forEach { call -> it.addDeferredCall(method, call) } @@ -43,7 +47,8 @@ class GoStateData( val otherCalls = other.deferredCalls.mapValues { it.value.toList() } val sameCalls = thisCalls == otherCalls if (flowStack.toList() != other.flowStack.toList() || !sameCalls || - arrayViews != other.arrayViews || pointerTargets != other.pointerTargets + arrayViews != other.arrayViews || pointerTargets != other.pointerTargets || + pointerConversions != other.pointerConversions ) { return null } @@ -56,3 +61,9 @@ class GoStateData( return newStack } } + +internal data class GoPointerConversion( + val source: UHeapRef, + val sourceType: GoType, + val targetType: GoType, +) diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt new file mode 100644 index 0000000000..5d0bac7759 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt @@ -0,0 +1,54 @@ +package org.usvm.state + +import io.ksmt.utils.asExpr +import org.jacodb.go.api.ArrayType +import org.jacodb.go.api.GoType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.StructType +import org.usvm.UExpr +import org.usvm.USort +import org.usvm.api.readField +import org.usvm.memory.allocateGoArrayInitialized +import org.usvm.memory.arrayView +import org.usvm.memory.readGoArrayIndex +import org.usvm.mkSizeAddExpr +import org.usvm.mkSizeExpr +import org.usvm.sizeSort +import org.usvm.type.underlying + +internal fun GoState.copyValue(value: UExpr, type: GoType): UExpr = with(ctx) { + val underlying = type.underlying() + if (underlying !is StructType && underlying !is ArrayType) return value + + val reference = value.asExpr(addressSort) + if (type is NamedType) { + return box(copyValue(unbox(reference, addressSort), underlying), type) + } + + return when (underlying) { + is StructType -> { + val fields = underlying.fields.orEmpty().mapIndexed { index, fieldType -> + copyValue(memory.readField(reference, index, typeToSort(fieldType)), fieldType) + } + mkTuple(type, fields = fields.toTypedArray()) + } + is ArrayType -> { + val view = arrayView(reference, type) + val elementType = underlying.elementType + val sort = typeToSort(elementType) + val fields = (0 until underlying.len.toInt()).asSequence().map { index -> + val element = memory.readGoArrayIndex( + view.backing, + mkSizeAddExpr(view.offset, mkSizeExpr(index)), + view.storageType, + sort, + ) + copyValue(element, elementType).asExpr(sort) + } + memory.allocateGoArrayInitialized(type, sort, sizeSort, fields) + } + else -> { + value + } + } +} diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoValueShape.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoValueShape.kt new file mode 100644 index 0000000000..cbbe2d1008 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoValueShape.kt @@ -0,0 +1,69 @@ +package org.usvm.state + +import io.ksmt.utils.asExpr +import org.jacodb.go.api.ArrayType +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.usvm.UBoolExpr +import org.usvm.UExpr +import org.usvm.USort +import org.usvm.api.readField +import org.usvm.isTrue +import org.usvm.memory.readGoArrayLength +import org.usvm.mkSizeExpr +import org.usvm.mkSizeGeExpr +import org.usvm.sizeSort +import org.usvm.type.GoBasicTypes +import org.usvm.type.underlying + +internal fun GoState.valueShape(value: UExpr, type: GoType): UBoolExpr = with(ctx) { + if (value.sort != addressSort) return trueExpr + + val reference = value.asExpr(addressSort) + val isNil = mkHeapRefEq(reference, nullRef) + if (isNil.isTrue) return if (type.isNilable() || type.underlying() == GoBasicTypes.STRING) trueExpr else falseExpr + + if (type is NamedType && type.underlying() !is InterfaceType) { + val underlying = type.underlying() + val payload = unbox(reference, typeToSort(underlying)) + val shape = mkAnd(isBoxed(reference), valueShape(payload, underlying)) + return if (underlying.isNilable()) mkOr(isNil, shape) else mkAnd(mkNot(isNil), shape) + } + + return when (val underlyingType = type.underlying()) { + is StructType -> { + val fields = underlyingType.fields.orEmpty().mapIndexed { index, fieldType -> + valueShape(memory.readField(reference, index, typeToSort(fieldType)), fieldType) + } + mkAnd(listOf(mkNot(isNil)) + fields) + } + is ArrayType -> { + mkNot(isNil) + } + is PointerType -> { + mkOr(isNil, isPointer(reference)) + } + is InterfaceType -> { + val boxed = mkAnd(isBoxed(reference), memory.types.evalIsSubtype(reference, underlyingType)) + mkOr(isNil, boxed) + } + is SliceType, GoBasicTypes.STRING -> { + val length = memory.readGoArrayLength(reference, type, sizeSort) + mkOr(isNil, mkSizeGeExpr(length, mkSizeExpr(0))) + } + else -> { + trueExpr + } + } +} + +internal fun GoType.isNilable(): Boolean = when (underlying()) { + is PointerType, is SliceType, is MapType, is InterfaceType, is SignatureType -> true + else -> false +} 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 7598b7cebb..5917d8668d 100644 --- a/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt @@ -7,10 +7,8 @@ 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 @@ -39,7 +37,8 @@ class GoTypeSystem( isInstantiable(type.underlyingType) } is PointerType -> { - isInstantiable(type.baseType) + // A pointer value is valid even when its pointee is an interface or a recursive pointer type. + true } else -> { false @@ -47,29 +46,16 @@ class GoTypeSystem( } 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 - } + is InterfaceType -> false + is NamedType -> type.underlying() !is InterfaceType + else -> true } - 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 hasCommonSubtype(type: GoType, types: Collection): Boolean { + if (isFinal(type)) return types.all { isSupertype(it, type) } + + return findSubtypes(type).any { candidate -> + isInstantiable(candidate) && types.all { isSupertype(it, candidate) } } } @@ -99,7 +85,7 @@ class GoTypeSystem( impl.methods.containsAll(iface.methods) } impl is PointerType -> { - implements(iface, impl.baseType) + impl.baseType.underlying() !is InterfaceType && implements(iface, impl.baseType) } else -> { false diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt new file mode 100644 index 0000000000..148eee4204 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt @@ -0,0 +1,114 @@ +package org.usvm.samples + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import mu.KotlinLogging +import org.junit.jupiter.api.Test +import org.usvm.generatedGoFile +import org.usvm.interpreter.GoPointer +import java.nio.file.Files +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class GoExamplesReplayTest : GoMethodTestRunner() { + @Test + fun sliceInputAndMutation() = replay(method = "sliceOverwrite") + + @Test + fun sliceInputAndCopy() = replay(method = "sliceCopySimple") + + @Test + fun arrayInput() = replay(method = "arrayIndex") + + @Test + fun pointerInputAndConversion() = replay(method = "pointerChangeType") + + @Test + fun objectInputAndMutation() = replay(method = "(*usvm/examples.Object).Set") + + private fun replay(method: String) { + GoExamplesReplay.replay(method = method, executions = runner(method, options)) + } +} + +internal object GoExamplesReplay { + private val logger = KotlinLogging.logger {} + + fun replay(method: String, executions: List) { + assertTrue(executions.isNotEmpty(), "Replay needs generated inputs") + val request = buildJsonObject { + put("method", method) + put( + "inputs", + buildJsonArray { executions.forEach { add(toJson(it.arguments)) } } + ) + } + + val replayFile = Files.createTempFile("usvm-go-examples-replay-", ".json").toFile() + val logFile = Files.createTempFile("usvm-go-examples-replay-", ".log").toFile() + try { + replayFile.writeText(request.toString()) + val builder = ProcessBuilder( + generatedGoFile("native-examples-replay.test").path, + "-test.run=^TestReplayExamples$" + ).redirectErrorStream(true).redirectOutput(logFile) + builder.environment()["USVM_GO_REPLAY_FILE"] = replayFile.path + val process = builder.start() + try { + assertTrue(process.waitFor(10, TimeUnit.SECONDS), "Native replay timed out") + assertEquals(expected = 0, actual = process.exitValue(), message = logFile.readText()) + } finally { + process.destroyForcibly() + } + + val nativeResults = Json.parseToJsonElement(replayFile.readText()).jsonArray + assertEquals(executions.size, nativeResults.size) + executions.zip(nativeResults).forEachIndexed { index, (execution, native) -> + val result = native.jsonObject + assertEquals( + expected = execution.result.isPanic, + actual = result.getValue("isPanic").jsonPrimitive.boolean, + message = "$method witness $index panic" + ) + if (execution.result.isSuccess) { + assertEquals( + expected = toJson(execution.result.value), + actual = result.getValue("value"), + message = "$method witness $index result" + ) + } + assertEquals( + expected = toJson(execution.argumentsAfter), + actual = result.getValue("argumentsAfter"), + message = "$method witness $index arguments after" + ) + } + logger.info { "Replayed ${executions.size} generated inputs for $method with native Go" } + } finally { + replayFile.delete() + logFile.delete() + } + } + + private fun toJson(value: Any?): JsonElement = when (value) { + null -> JsonNull + is GoPointer -> toJson(value.value) + is Number -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is String -> JsonPrimitive(value) + is List<*> -> JsonArray(value.map(::toJson)) + is Map<*, *> -> buildJsonObject { value.forEach { (key, element) -> put(key.toString(), toJson(element)) } } + else -> error("Unsupported native replay value: $value") + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt index 2c6e3d8cda..06732e5123 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt @@ -60,6 +60,7 @@ class SampleCoverageTest { org.usvm.samples.globals.GlobalsTest::class.java, org.usvm.samples.objects.ObjectsTest::class.java, org.usvm.samples.pointers.PointersTest::class.java, + org.usvm.samples.pointers.PointerRegressionTest::class.java, org.usvm.samples.strings.NamedStringsTest::class.java, org.usvm.samples.strings.StringConstraintsTest::class.java, org.usvm.samples.strings.StringRegressionTest::class.java, @@ -67,6 +68,7 @@ class SampleCoverageTest { org.usvm.samples.types.InterfacesTest::class.java, org.usvm.samples.types.NamedTypesTest::class.java, org.usvm.samples.types.StructsTest::class.java, + org.usvm.samples.types.ValueRegressionTest::class.java, org.usvm.samples.unsupported.GoUnsupportedTest::class.java, ) } diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt index 2e00d4b36c..9b479b6622 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt @@ -2,10 +2,12 @@ package org.usvm.samples.collections.maps import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test +import org.usvm.samples.GoExamplesReplay import org.usvm.samples.GoMap import org.usvm.samples.GoMethodTestRunner import org.usvm.samples.GoResult import org.usvm.samples.GoSample +import org.usvm.samples.longEntry import org.usvm.samples.longValue import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults @@ -24,10 +26,21 @@ class SlowMapIterationTest : GoMethodTestRunner() { val expected = if (values.size < 4) { -1L } else { - nativeInt(maxOf(0L, values.max()) - minOf(0L, values.min())) + val maximum = maxOf(0L, values.max()) + val minimum = minOf(0L, values.min()) + val atZero = m.longEntry(key = 0L) + if (maximum == minimum) { + minimum + } else { + val upper = if (maximum > 0L) maximum else atZero + val lower = if (minimum < 0L) minimum else atZero + nativeInt(upper - lower) + } } r.long == expected }, ) + + GoExamplesReplay.replay(method = "mapLoopLen", executions = runner("mapLoopLen", options)) } } diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointerRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointerRegressionTest.kt new file mode 100644 index 0000000000..a0e353b27b --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/pointers/PointerRegressionTest.kt @@ -0,0 +1,23 @@ +package org.usvm.samples.pointers + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class PointerRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "nilPointerConversion", fixture = "regressions") + fun nilPointerConversion() = checkNative(method = "nilPointerConversion") + + @Test + @GoSample(method = "pointerConversionAlias", fixture = "regressions") + fun pointerConversionAlias() = checkNative(method = "pointerConversionAlias") + + @Test + @GoSample(method = "pointerConversionRoundTrip", fixture = "regressions") + fun pointerConversionRoundTrip() = checkNative(method = "pointerConversionRoundTrip") + + @Test + @GoSample(method = "namedPointerConversionAlias", fixture = "regressions") + fun namedPointerConversionAlias() = checkNative(method = "namedPointerConversionAlias") +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt index ea4cbe95b8..2783fb713e 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt @@ -46,6 +46,9 @@ class InterfacesTest : GoMethodTestRunner() { @Test @GoSample(method = "assertCreatureFailNoComma") fun assertCreatureFailNoComma() { + // The return after this guaranteed failing assertion is unreachable in Go. + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + checkDiscoveredProperties( method = "assertCreatureFailNoComma", analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/types/ValueRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/types/ValueRegressionTest.kt new file mode 100644 index 0000000000..a7e7e27f8b --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/ValueRegressionTest.kt @@ -0,0 +1,47 @@ +package org.usvm.samples.types + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class ValueRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "structValueCopy", fixture = "regressions") + fun structValueCopy() = checkNative(method = "structValueCopy") + + @Test + @GoSample(method = "nestedStructValueCopy", fixture = "regressions") + fun nestedStructValueCopy() = checkNative(method = "nestedStructValueCopy") + + @Test + @GoSample(method = "arrayValueCopy", fixture = "regressions") + fun arrayValueCopy() = checkNative(method = "arrayValueCopy") + + @Test + @GoSample(method = "structArgumentCopy", fixture = "regressions") + fun structArgumentCopy() = checkNative(method = "structArgumentCopy") + + @Test + @GoSample(method = "arrayArgumentCopy", fixture = "regressions") + fun arrayArgumentCopy() = checkNative(method = "arrayArgumentCopy") + + @Test + @GoSample(method = "interfaceStructCopy", fixture = "regressions") + fun interfaceStructCopy() = checkNative(method = "interfaceStructCopy") + + @Test + @GoSample(method = "nilStructAssertionZero", fixture = "regressions") + fun nilStructAssertionZero() = checkNative(method = "nilStructAssertionZero") + + @Test + @GoSample(method = "typedNilPointerAssertion", fixture = "regressions") + fun typedNilPointerAssertion() = checkNative(method = "typedNilPointerAssertion") + + @Test + @GoSample(method = "nilStructAssertionOk", fixture = "regressions") + fun nilStructAssertionOk() = checkNative(method = "nilStructAssertionOk") + + @Test + @GoSample(method = "pointerToInterfaceDoesNotImplement", fixture = "regressions") + fun pointerToInterfaceDoesNotImplement() = checkNative(method = "pointerToInterfaceDoesNotImplement") +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt index 332c199ae7..2156bfc2a1 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt @@ -28,6 +28,13 @@ class GoUnsupportedTest : GoMethodTestRunner(fixture = "regressions") { message = "two symbolic lengths" ) + @Test + @GoSample(method = "oversizedResolvedSlice", fixture = "regressions") + fun oversizedSlicesAreRejectedWithoutTruncation() = assertUnsupported( + method = "oversizedResolvedSlice", + message = "within size limit" + ) + private fun assertUnsupported(method: String, message: String) { val error = assertThrows { runner(method, options) } assertTrue(error.message.orEmpty().contains(message), error.message) From 4863685df1ee3de898bdc59ae18c46582d00902f Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Fri, 9 Oct 2026 17:17:12 +0300 Subject: [PATCH 6/6] Fix Go defer, interface and aggregate value semantics --- .../usvm/api/collections/PrimitiveMapTest.kt | 4 +- usvm-go/README.md | 38 ++- usvm-go/src/main/go/model.go | 3 +- usvm-go/src/main/go/pack.go | 11 +- usvm-go/src/main/go/regressions/review.go | 248 ++++++++++++++++++ .../src/main/go/regressions/semantics_test.go | 32 +++ usvm-go/src/main/kotlin/org/usvm/GoContext.kt | 23 +- usvm-go/src/main/kotlin/org/usvm/GoDomain.kt | 7 +- .../src/main/kotlin/org/usvm/GoExprVisitor.kt | 151 ++++++++--- .../src/main/kotlin/org/usvm/GoInstVisitor.kt | 18 +- usvm-go/src/main/kotlin/org/usvm/GoMachine.kt | 2 +- .../org/usvm/interpreter/GoInterpreter.kt | 12 +- .../main/kotlin/org/usvm/model/Converter.kt | 13 +- .../src/main/kotlin/org/usvm/model/Model.kt | 1 + .../kotlin/org/usvm/state/GoArrayValueCopy.kt | 75 ++++++ .../src/main/kotlin/org/usvm/state/GoState.kt | 33 ++- .../main/kotlin/org/usvm/state/GoStateData.kt | 35 ++- .../main/kotlin/org/usvm/state/GoValueCopy.kt | 16 +- .../main/kotlin/org/usvm/type/GoTypeSystem.kt | 8 +- .../org/usvm/samples/GoExamplesReplayTest.kt | 69 ++--- .../kotlin/org/usvm/samples/GoNativeReplay.kt | 42 +++ .../org/usvm/samples/GoNativeTestRunner.kt | 42 +-- .../org/usvm/samples/SampleCoverageTest.kt | 18 +- .../maps/MapValueRegressionTest.kt | 39 +++ .../slices/CompositeSliceRegressionTest.kt | 93 +++++++ .../samples/exceptions/DeferRegressionTest.kt | 50 ++++ .../pointers/NamedPointerRegressionTest.kt | 11 + .../samples/types/InterfaceRegressionTest.kt | 70 +++++ 28 files changed, 963 insertions(+), 201 deletions(-) create mode 100644 usvm-go/src/main/go/regressions/review.go create mode 100644 usvm-go/src/main/kotlin/org/usvm/state/GoArrayValueCopy.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/GoNativeReplay.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapValueRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/CompositeSliceRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/pointers/NamedPointerRegressionTest.kt create mode 100644 usvm-go/src/test/kotlin/org/usvm/samples/types/InterfaceRegressionTest.kt diff --git a/usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt b/usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt index 9250037919..b8978b001a 100644 --- a/usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt +++ b/usvm-core/src/test/kotlin/org/usvm/api/collections/PrimitiveMapTest.kt @@ -42,8 +42,8 @@ class PrimitiveMapTest : SymbolicCollectionTestBase() { @Test fun symbolicKeyEqualityControlsSize() = scope.doWithState { val map = mkSymbolicObjectMap(mapType) - val first = ctx.mkRegisterReading(0, ctx.bv32Sort) - val second = ctx.mkRegisterReading(1, ctx.bv32Sort) + val first = ctx.mkRegisterReading(idx = 0, sort = ctx.bv32Sort) + val second = ctx.mkRegisterReading(idx = 1, sort = ctx.bv32Sort) val keyInfo = USizeExprKeyInfo() symbolicPrimitiveMapPut(map, first, ctx.mkBv(1), mapType, keyInfo) diff --git a/usvm-go/README.md b/usvm-go/README.md index 6b13d40a2f..a910bdcd8f 100644 --- a/usvm-go/README.md +++ b/usvm-go/README.md @@ -56,9 +56,9 @@ retain structured values and interface dynamic types; these snapshots do not pre object identity or the complete alias graph. Function parameters remain mocked: the `call` example checks that mock contract, rather than replaying an actual function body. -The thematic `*RegressionTest` classes compare results with native Go for **80 -zero-argument scalar/panic cases**. Branch, slice-alias and named-number/interface -checks replay seven generated concrete inputs in a native Go executable. +The thematic `*RegressionTest` classes compare results with native Go for **108 +zero-argument scalar/panic cases**. Branch, slice-alias, named-number/interface and +composite-copy checks replay 19 generated concrete inputs in a native Go executable. `GoExamplesReplayTest` additionally replays generated array, slice, pointer and object inputs against the original example functions, checking return values, panic occurrence and argument snapshots after execution. The manual map test replays the original @@ -81,7 +81,7 @@ instruction coverage but still enforce their semantic expectations. `Building` assertion always panics, so the following return is unreachable. Every collected execution must still satisfy its panic expectation. -The current local validation passes **201 default tests** and **5 manual tests**, with +The current local validation passes **237 default tests** and **5 manual tests**, with no failures or skipped tests. Detekt on Go main/test sources, `validateProjectList` and `git diff --check` pass. These counts describe a bounded local run, not an exhaustive proof or a count of independent semantics supported. @@ -90,25 +90,35 @@ The reproduced defects are now covered by symbolic expectations and selected nat comparisons: - Nil-map lookup, comma-ok, deletion, range and assignment panic, including named maps. - Absent comma-ok lookups return zero. Integer keys/values have native regressions; a - symbolic test requires nil, absent-key and present-key witnesses. + Absent lookups return type-specific zero values, including structs, arrays and named + scalars. Map insertion and lookup copy struct/array values. Integer keys/values have + native regressions; a symbolic test requires nil, absent-key and present-key witnesses. - Named scalars, unary operators, interface assertions, argument snapshots, unsigned integer-to-float conversion, finite representable float-to-integer truncation and exact float-literal export. All 34 arithmetic/named-number tests pass. - Input shapes and snapshots for arrays, structs, named values, interfaces, maps and slices; valid representation tags are constraints rather than artificial Go panics. -- Array/struct copying on assignment, calls and interface boxing, including nested - structs. Native regressions check that changing a copy preserves the original. +- Array/struct copying on assignment, calls, interface boxing, map insertion/lookup, + `copy` and `append`, including nested structs. Native regressions check independent + value fields and shared pointees inside copied structs. Composite slice copies read + a source snapshot and advance one element per machine step, supporting overlapping + ranges and symbolic lengths within the normal analysis budgets. +- Deferred calls capture arguments when registered and keep a separate defer stack for + each invocation. Native regressions cover repeated calls, value arguments and recursion. - Pointer conversions preserve nil and a shared pointee. Native regressions check aliasing in both conversion directions and round-trip equality. - Comma-ok assertions produce composite zero values; failed non-comma assertions panic. Interface calls explore admissible concrete receivers and typed nil pointer panics. - Pointers to interfaces do not acquire the interface's methods. + Value and pointer method sets are exported separately; a value does not acquire + pointer-receiver methods. Pointers to interfaces do not acquire interface methods. + Named nilable values compare their payload with nil, including failed assertions. - Model resolution refines oversized collection witnesses within the same path constraints. If no witness fits the materialization limit, resolution reports unsupported rather than truncating the value and presenting it as a successful concrete result. -- Slice bounds use direct comparisons without overflowing `limit + 1`. The map-iteration - expectation follows the source's zero-initialized keys, independently checked by replay. +- Slice bounds use direct comparisons without overflowing `limit + 1`; narrow signed + indices must be nonnegative before widening. Stores through nil pointers panic. + The map-iteration expectation follows the source's zero-initialized keys, independently + checked by replay. `generateGoImports` can export the import examples for investigation. The original import/standard-library exploratory factories depended on manually prepared dumps; @@ -142,9 +152,9 @@ support. In particular: - Unknown external calls and function parameters retain the prototype's mocking behavior. `GoFunctionReference` identifies a function input that cannot be replayed. - Selected array/struct copies and scalar-pointer conversions have native validation. - General pointer/interface identity and equality, reference map keys, and zero values - for composite/named map values still need broader validation. Nil-map regressions - currently cover integer keys/values. + General pointer/interface identity and equality and reference map keys still need + broader validation. Map-zero regressions cover selected struct, array and named scalar + values; nil-map regressions currently cover integer keys/values. - Package loading, standard-library integration and arbitrary repository workflows have not been validated end to end. Instruction coverage alone is not a semantic correctness check. diff --git a/usvm-go/src/main/go/model.go b/usvm-go/src/main/go/model.go index dcd0298448..2992d4f69c 100644 --- a/usvm-go/src/main/go/model.go +++ b/usvm-go/src/main/go/model.go @@ -471,7 +471,8 @@ type Named struct { type Pointer struct { CommonType `yaml:"-,inline"` - Elem string `yaml:"elem" json:"elem"` + Elem string `yaml:"elem" json:"elem"` + Methods []string `yaml:"methods,omitempty" json:"methods,omitempty"` } type Signature struct { diff --git a/usvm-go/src/main/go/pack.go b/usvm-go/src/main/go/pack.go index 7e55e7b0ca..994d3d4548 100644 --- a/usvm-go/src/main/go/pack.go +++ b/usvm-go/src/main/go/pack.go @@ -94,11 +94,13 @@ func (p *Package) AddType(typ types.Type) { p.Members = append(p.Members, p.PackMember(f)) } p.AddType(t.Underlying()) + p.AddType(types.NewPointer(t)) case *types.Pointer: common.Type = PointerType p.Types[name] = Pointer{ CommonType: common, Elem: t.Elem().String(), + Methods: p.PackMethods(t), } p.AddType(t.Elem()) case *types.Signature: @@ -632,10 +634,11 @@ 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) +func (p *Package) PackMethods(in types.Type) []string { + methodSet := types.NewMethodSet(in) + methods := make([]string, 0, methodSet.Len()) + for i := 0; i < methodSet.Len(); i++ { + method := methodSet.At(i).Obj() signature := method.Type().(*types.Signature) methods = append(methods, method.Name()+p.PackMethodParams(signature.Params())+p.PackMethodResults(signature.Results())) } diff --git a/usvm-go/src/main/go/regressions/review.go b/usvm-go/src/main/go/regressions/review.go new file mode 100644 index 0000000000..d198610f79 --- /dev/null +++ b/usvm-go/src/main/go/regressions/review.go @@ -0,0 +1,248 @@ +package regressions + +func reviewDeferSet(pointer *int, value int) { *pointer = value } + +func deferredArguments() (result int) { + defer reviewDeferSet(&result, 1) + reviewDeferSet(&result, 2) + return +} + +func repeatedDeferArguments() (result int) { + defer reviewDeferSet(&result, 1) + defer reviewDeferSet(&result, 2) + return +} + +func deferredStructArgument() (result int) { + value := valueRecord{number: 1} + defer func(captured valueRecord) { result = captured.number }(value) + value.number = 2 + return +} + +func recursiveDeferFrames(depth int) (result int) { + if depth < 0 || depth > 2 { + return -1 + } + defer func() { result++ }() + if depth > 0 { + result = recursiveDeferFrames(depth - 1) + } + return +} + +func recursiveDeferredArguments() int { + return recursiveDeferFrames(2) + recursiveDeferFrames(-1) + recursiveDeferFrames(3) +} + +type reviewError string + +func (value reviewError) Error() string { return string(value) } + +func interfaceAssertion() bool { + var value any = reviewError("ok") + _, ok := value.(error) + return ok +} + +func interfaceAssertionNoComma() bool { + var value any = reviewError("ok") + return value.(error) != nil +} + +type reviewReader interface{ Read() int } +type reviewCounter struct{ number int } + +func (counter *reviewCounter) Read() int { return counter.number } + +func pointerInterfaceCall() int { + var reader reviewReader = &reviewCounter{number: 1} + return reader.Read() +} + +func valueHasPointerMethods() bool { + var value any = reviewCounter{number: 1} + _, ok := value.(reviewReader) + return ok +} + +func symbolicInterfaceReceiver(reader reviewReader) int { return reader.Read() } + +func mapStructCopy() int { + original := valueRecord{number: 1} + values := map[int]valueRecord{0: original} + original.number = 2 + return values[0].number +} + +func mapArrayCopy() int { + original := [1]int{1} + values := map[int][1]int{0: original} + original[0] = 2 + return values[0][0] +} + +func mapLookupStructCopy() int { + values := map[int]valueRecord{0: {number: 1}} + copied := values[0] + copied.number = 2 + return values[0].number +} + +func missingStructLookup() int { + values := map[int]valueRecord{} + return values[0].number +} + +func missingNamedLookup() int { + values := map[int]namedNumber{} + return int(values[0]) +} + +func missingArrayLookup() int { + values := map[int][1]int{} + return values[0][0] +} + +func missingStructLookupComma() int { + values := map[int]valueRecord{} + value, _ := values[0] + return value.number +} + +func sliceStructCopy() int { + source := []valueRecord{{number: 1}} + destination := make([]valueRecord, 1) + copy(destination, source) + destination[0].number = 2 + return source[0].number +} + +func appendStructCopy() int { + source := []valueRecord{{number: 1}} + destination := append([]valueRecord{}, source...) + destination[0].number = 2 + return source[0].number +} + +func sliceArrayCopy() int { + source := [][1]int{{1}} + destination := make([][1]int, 1) + copy(destination, source) + destination[0][0] = 2 + return source[0][0] +} + +func appendArrayCopy() int { + source := [][1]int{{1}} + destination := append([][1]int{}, source...) + destination[0][0] = 2 + return source[0][0] +} + +func overlapCompositeCopy() int { + values := []valueRecord{{number: 1}, {number: 2}, {number: 3}} + copy(values[1:], values) + values[1].number = 9 + return values[0].number + values[1].number + values[2].number +} + +func appendCompositeReuse() int { + original := make([]valueRecord, 1, 2) + original[0] = valueRecord{number: 1} + source := []valueRecord{{number: 2}} + destination := append(original, source...) + destination[1].number = 9 + destination[0].number = 3 + return source[0].number*10 + original[0].number +} + +func appendCompositeAllocate() int { + source := []valueRecord{{number: 1}} + destination := append(source, valueRecord{number: 2}) + destination[0].number = 9 + return source[0].number +} + +func narrowSignedIndex() int { + values := make([]int, 256) + index := int8(-1) + return values[index] +} + +func negativeInt16Index() int { + values := make([]int, 65536) + index := int16(-1) + return values[index] +} + +func symbolicNarrowIndex(index int8) int { + values := make([]int, 256) + return values[index] +} + +func symbolicCompositeCopy(length int) int { + if length < 0 || length > 3 { + return -1 + } + source := []valueRecord{{number: 1}, {number: 2}, {number: 3}} + destination := make([]valueRecord, length) + count := copy(destination, source) + if count > 0 { + destination[0].number = 9 + } + return count*10 + source[0].number +} + +func symbolicCompositeAppend(length int) int { + if length < 0 || length > 3 { + return -1 + } + source := []valueRecord{{number: 1}, {number: 2}, {number: 3}} + destination := append([]valueRecord{}, source[:length]...) + if length > 0 { + destination[0].number = 9 + } + return len(destination)*10 + source[0].number +} + +func namedMapAssertionZero() bool { + type namedMap map[int]int + var boxed any + value, _ := boxed.(namedMap) + return value == nil +} + +func namedSliceAssertionZero() bool { + type namedSlice []int + var boxed any + value, _ := boxed.(namedSlice) + return value == nil +} + +func namedPointerAssertionZero() bool { + type namedPointer *int + var boxed any + value, _ := boxed.(namedPointer) + return value == nil +} + +func compositePointerFieldsRemainShared() int { + type record struct { + number int + pointer *int + } + + pointed := 1 + source := []record{{number: 1, pointer: &pointed}} + destination := make([]record, 1) + copy(destination, source) + appended := append([]record{}, source...) + values := map[int]record{0: source[0]} + + destination[0].number = 9 + *destination[0].pointer = 3 + + return source[0].number*1000 + *source[0].pointer*100 + appended[0].number*10 + *values[0].pointer +} diff --git a/usvm-go/src/main/go/regressions/semantics_test.go b/usvm-go/src/main/go/regressions/semantics_test.go index e9458313e3..1a7802fcc2 100644 --- a/usvm-go/src/main/go/regressions/semantics_test.go +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -9,6 +9,34 @@ import ( func TestNativeOracle(t *testing.T) { cases := map[string]func() any{ + "compositePointerFieldsRemainShared": func() any { return compositePointerFieldsRemainShared() }, + "deferredArguments": func() any { return deferredArguments() }, + "repeatedDeferArguments": func() any { return repeatedDeferArguments() }, + "deferredStructArgument": func() any { return deferredStructArgument() }, + "recursiveDeferredArguments": func() any { return recursiveDeferredArguments() }, + "interfaceAssertion": func() any { return interfaceAssertion() }, + "interfaceAssertionNoComma": func() any { return interfaceAssertionNoComma() }, + "pointerInterfaceCall": func() any { return pointerInterfaceCall() }, + "valueHasPointerMethods": func() any { return valueHasPointerMethods() }, + "mapStructCopy": func() any { return mapStructCopy() }, + "mapArrayCopy": func() any { return mapArrayCopy() }, + "mapLookupStructCopy": func() any { return mapLookupStructCopy() }, + "missingStructLookup": func() any { return missingStructLookup() }, + "missingNamedLookup": func() any { return missingNamedLookup() }, + "missingArrayLookup": func() any { return missingArrayLookup() }, + "missingStructLookupComma": func() any { return missingStructLookupComma() }, + "namedMapAssertionZero": func() any { return namedMapAssertionZero() }, + "sliceStructCopy": func() any { return sliceStructCopy() }, + "appendStructCopy": func() any { return appendStructCopy() }, + "sliceArrayCopy": func() any { return sliceArrayCopy() }, + "appendArrayCopy": func() any { return appendArrayCopy() }, + "overlapCompositeCopy": func() any { return overlapCompositeCopy() }, + "appendCompositeReuse": func() any { return appendCompositeReuse() }, + "appendCompositeAllocate": func() any { return appendCompositeAllocate() }, + "narrowSignedIndex": func() any { return narrowSignedIndex() }, + "negativeInt16Index": func() any { return negativeInt16Index() }, + "namedSliceAssertionZero": func() any { return namedSliceAssertionZero() }, + "namedPointerAssertionZero": func() any { return namedPointerAssertionZero() }, "namedNegation": func() any { return namedNegation() }, "namedComplement": func() any { return namedComplement() }, "namedBooleanNot": func() any { return namedBooleanNot() }, @@ -131,6 +159,10 @@ func TestReplaySymbolicInputs(t *testing.T) { outputs[index] = symbolicBranch(input) case "symbolicSliceAlias": outputs[index] = symbolicSliceAlias(input) + case "symbolicCompositeCopy": + outputs[index] = symbolicCompositeCopy(input) + case "symbolicCompositeAppend": + outputs[index] = symbolicCompositeAppend(input) case "symbolicNamedInterfaceRoundTrip": outputs[index] = int(symbolicNamedInterfaceRoundTrip(namedNumber(input))) default: diff --git a/usvm-go/src/main/kotlin/org/usvm/GoContext.kt b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt index 845563a538..d5a85d6847 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoContext.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt @@ -27,19 +27,12 @@ class GoContext( private val methodInfo: MutableMap = hashMapOf() private val globals: MutableMap> = hashMapOf() - fun getMethodInfo(method: GoMethod) = methodInfo.getValue(method) - - fun setMethodInfo(method: GoMethod) { + fun getMethodInfo(method: GoMethod): GoMethodInfo = methodInfo.getOrPut(method) { 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)) + GoMethodInfo( + variablesCount = localsCount + getFreeVariablesCount(method), + argumentsCount = method.parameters.size, + ) } fun addGlobal(global: GoGlobal, expr: UExpr) { @@ -97,11 +90,7 @@ class GoContext( } } - private fun setMethodInfo(method: GoMethod, info: GoMethodInfo) { - methodInfo[method] = info - } - - private fun getArgsCount(method: GoMethod): Int = methodInfo.getValue(method).argumentsCount + private fun getArgsCount(method: GoMethod): Int = getMethodInfo(method).argumentsCount private fun getFreeVariablesCount(method: GoMethod): Int = when (method) { is GoFunction -> { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt index 71660a5932..f40248b41e 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt @@ -13,16 +13,17 @@ 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( +data class GoCall( val method: GoMethod, val entrypoint: GoInst, + val arguments: List>? = null, + val freeVariables: List> = emptyList(), ) class GoPackage( @@ -31,6 +32,7 @@ class GoPackage( val globals: List, val types: Map, val intSize: Int, + val methodSets: Map> = emptyMap(), ) { private val methodsIndex: Map = methods.associateBy { it.metName } @@ -46,6 +48,7 @@ class GoProgram(packages: List) { val globals = packages.flatMap { it.globals } val types = packages.flatMap { it.types.entries }.associate { it.key to it.value } + val methodSets = packages.flatMap { it.methodSets.entries }.associate { it.key to it.value } private val packagesIndex: Map = packages.associateBy { it.name } diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index 6fce001a23..768238fc32 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -88,6 +88,7 @@ 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.StructType import org.jacodb.go.api.TupleType import org.usvm.api.UnknownBinaryOperationException import org.usvm.api.UnknownFunctionException @@ -129,6 +130,8 @@ import org.usvm.memory.readGoArrayLength import org.usvm.operator.GoBinaryOperator import org.usvm.operator.GoUnaryOperator import org.usvm.operator.mkNarrow +import org.usvm.state.GoArrayCopyOperation +import org.usvm.state.GoArrayValueCopy import org.usvm.state.GoMethodResult import org.usvm.state.GoState.Companion.POINTER_FIELD import org.usvm.state.copyValue @@ -193,15 +196,13 @@ class GoExprVisitor( return mockCall(expr, method) } - val parameters = args.map { it.accept(this) }.toTypedArray() - val call = GoCall(method, applicationGraph.entryPoints(method).first()) + val parameters = args.map { it.accept(this) } parameters.forEachIndexed { index, value -> val parameterType = method.parameters[index].type as GoType scope.assert(scope.calcOnState { valueShape(value, parameterType) }) ?: throw GoStepAbort() } - ctx.setMethodInfo(method, parameters) - scope.doWithState { + val call = createCall(method, applicationGraph.entryPoints(method).first(), parameters) addCall(call, currentStatement) } return ctx.noValue @@ -255,8 +256,8 @@ class GoExprVisitor( argumentShapes ) cases += condition to { - ctx.setMethodInfo(method, (listOf(value) + args).toTypedArray()) - addCall(GoCall(method, applicationGraph.entryPoints(method).first()), currentStatement) + val call = createCall(method, applicationGraph.entryPoints(method).first(), listOf(value) + args) + addCall(call, currentStatement) } } @@ -435,7 +436,6 @@ class GoExprVisitor( override fun visitGoMakeMapExpr(expr: GoMakeMapExpr): UExpr { val mapType = expr.type.underlying() - return scope.calcOnState { val ref = memory.allocConcrete(mapType) memory.write(UMapLengthLValue(ref, mapType, ctx.sizeSort), ctx.mkSizeExpr(0), ctx.trueExpr) @@ -589,8 +589,8 @@ class GoExprVisitor( } val value = ctx.mkIte( contains, - trueBranch = { scope.calcOnState { memory.read(lvalue).asExpr(valueSort) } }, - falseBranch = { valueSort.sampleUValue() } + trueBranch = { scope.calcOnState { copyValue(memory.read(lvalue), mapType.valueType).asExpr(valueSort) } }, + falseBranch = { scope.calcOnState { sampleValue(mapType.valueType).asExpr(valueSort) } } ) return scope.calcOnState { @@ -716,7 +716,12 @@ class GoExprVisitor( scope.assert(isBoxed(boxed)) ?: throw GoStepAbort() val matches = scope.calcOnState { - ctx.mkAnd(ctx.mkNot(isNil), memory.types.evalIsSupertype(boxed, type)) + val typeMatches = if (type.underlying() is InterfaceType) { + memory.types.evalIsSubtype(boxed, type) + } else { + memory.types.evalIsSupertype(boxed, type) + } + ctx.mkAnd(ctx.mkNot(isNil), typeMatches) } val value = if (type is NamedType || type.underlying() is InterfaceType) { boxed @@ -899,12 +904,14 @@ class GoExprVisitor( val bothReferences = lhv.sort == ctx.addressSort && rhv.sort == ctx.addressSort val pointerOperands = expr.lhv.type.underlying() is PointerType || expr.rhv.type.underlying() is PointerType if (isEquality && bothReferences && (hasNilOperand || pointerOperands)) { - val equal = referenceEquals(lhv.asExpr(ctx.addressSort), rhv.asExpr(ctx.addressSort)) + val left = unboxNamedRef(lhv.asExpr(ctx.addressSort), expr.lhv.type) + val right = unboxNamedRef(rhv.asExpr(ctx.addressSort), expr.rhv.type) + val equal = referenceEquals(left, right) return if (expr is GoEqlExpr) equal else ctx.mkNot(equal) } val operandType = expr.lhv.type.underlying() - val signed = operandType is BasicType && !operandType.typeName.startsWith("ui") + val signed = operandType is BasicType && !operandType.typeName.startsWith("uint") val lhs = unboxNamedPrimitive(lhv, expr.lhv.type) val rhs = unboxNamedPrimitive(rhv, expr.rhv.type) if ((expr is GoDivExpr || expr is GoModExpr) && rhs.sort is UBvSort) { @@ -1010,6 +1017,12 @@ class GoExprVisitor( val arrayType = instance.type.let { if (it is PointerType) it.baseType else it }.underlying() val originalIndex = bv(unboxNamedPrimitive(idx.accept(this), idx.type)) val view = scope.calcOnState { arrayView(array, arrayType) } + val signed = !(idx.type.underlying() as BasicType).typeName.startsWith("uint") + if (signed) { + val nonnegative = ctx.mkBvSignedGreaterOrEqualExpr(originalIndex, ctx.mkBv(0, originalIndex.sort)) + scope.fork(nonnegative, blockOnFalseState = { panic("index out of bounds") }) ?: throw GoStepAbort() + } + val width = maxOf(originalIndex.sort.sizeBits, view.length.sort.sizeBits).toInt() val fullIndex = originalIndex.mkNarrow(width, signed = false) val fullLength = bv(view.length).mkNarrow(width, signed = false) @@ -1158,34 +1171,18 @@ class GoExprVisitor( @Suppress("ThrowsCount") // Failed scope checks stop this hop. private fun callBuiltin(method: GoBuiltin, args: List, returnType: GoType): UExpr { + val completed = scope.calcOnState { + data.builtinResult?.also { data.builtinResult = null } + } + if (completed != null) return tryBox(completed, returnType) + return when (method.name) { "append" -> { - tryBox(appendArray(args[0], args[1]), returnType) + val result = appendArray(args[0], args[1]) + if (result == ctx.noValue) result else tryBox(result, returnType) } "copy" -> { - val source = unboxNamedRef(args[1].accept(this).asExpr(ctx.addressSort), args[1].type) - val destination = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) - val sliceType = args[0].type.underlying() as SliceType - val count = scope.calcOnState { - val sourceView = arrayView(source, args[1].type) - val destinationView = arrayView(destination, args[0].type) - val copied = ctx.mkIte( - ctx.mkSizeLtExpr(sourceView.length, destinationView.length), - sourceView.length, - destinationView.length - ) - memory.copyGoArray( - sourceView.backing, - destinationView.backing, - sourceView.storageType, - ctx.typeToSort(sliceType.elementType), - sourceView.offset, - destinationView.offset, - copied - ) - copied - } - ctx.mkPrimitiveCast(count, ctx.typeToSort(returnType)) + copyArray(args, returnType) } "delete" -> { @@ -1262,6 +1259,40 @@ class GoExprVisitor( } } + private fun copyArray(args: List, returnType: GoType): UExpr { + val source = unboxNamedRef(args[1].accept(this).asExpr(ctx.addressSort), args[1].type) + val destination = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) + val elementType = (args[0].type.underlying() as SliceType).elementType + + return scope.calcOnState { + val sourceView = arrayView(source, args[1].type) + val destinationView = arrayView(destination, args[0].type) + val count = ctx.mkIte( + ctx.mkSizeLtExpr(sourceView.length, destinationView.length), + sourceView.length, + destinationView.length, + ) + val result = ctx.mkPrimitiveCast(count, ctx.typeToSort(returnType)) + + if (elementType.underlying() is StructType || elementType.underlying() is ArrayType) { + val copy = GoArrayValueCopy(sourceView, destinationView, count, elementType) + data.pendingArrayCopy = GoArrayCopyOperation(clone(), listOf(copy), result) + ctx.noValue + } else { + memory.copyGoArray( + sourceView.backing, + destinationView.backing, + sourceView.storageType, + ctx.typeToSort(elementType), + sourceView.offset, + destinationView.offset, + count, + ) + result + } + } + } + private fun copyMap( srcMap: UHeapRef, mapType: MapType, @@ -1344,6 +1375,38 @@ class GoExprVisitor( } } + private fun appendCompositeArray( + view: GoArrayView, + appendedView: GoArrayView, + type: GoType, + elementType: GoType, + newBacking: UHeapRef, + length: UExpr, + ): UExpr = scope.calcOnState { + val zero = ctx.mkSizeExpr(0) + val reuse = ctx.mkSizeLeExpr(length, view.capacity) + val backing = ctx.mkIte(reuse, view.backing, newBacking) + val offset = ctx.mkIte(reuse, view.offset, zero) + val header = memory.allocateGoArray(type, ctx.sizeSort, length) + val resultView = GoArrayView( + backing, + view.storageType, + offset, + length, + ctx.mkIte(reuse, view.capacity, length), + ) + data.arrayViews[header] = resultView + val newView = GoArrayView(newBacking, view.storageType, zero, length, length) + val appendedOffset = ctx.mkSizeAddExpr(offset, view.length) + val copies = listOf( + GoArrayValueCopy(view, newView, view.length, elementType), + GoArrayValueCopy(appendedView, newView.copy(offset = view.length), appendedView.length, elementType), + GoArrayValueCopy(appendedView, resultView.copy(offset = appendedOffset), appendedView.length, elementType), + ) + data.pendingArrayCopy = GoArrayCopyOperation(clone(), copies, header) + ctx.noValue + } + private fun appendArray(sliceValue: GoValue, appendValue: GoValue): UExpr { val slice = unboxNamedRef(sliceValue.accept(this).asExpr(ctx.addressSort), sliceValue.type) val appended = unboxNamedRef(appendValue.accept(this).asExpr(ctx.addressSort), appendValue.type) @@ -1357,6 +1420,19 @@ class GoExprVisitor( val length = ctx.mkSizeAddExpr(view.length, appendedView.length) checkLength(length) ?: throw GoStepAbort() val newBacking = memory.allocateGoArray(type, ctx.sizeSort, length) + val elementType = (type as? SliceType)?.elementType + val copiesValues = elementType?.underlying() is StructType || elementType?.underlying() is ArrayType + if (copiesValues) { + return@calcOnState appendCompositeArray( + view, + appendedView, + type, + checkNotNull(elementType), + newBacking, + length, + ) + } + memory.copyGoArray(view.backing, newBacking, view.storageType, elementSort, view.offset, zero, view.length) memory.copyGoArray( appendedView.backing, @@ -1438,7 +1514,8 @@ class GoExprVisitor( signature.results } } - val method = GoFunction(signature, emptyList(), funcName, emptyList(), "", emptyList(), emptyList()) + val method = + GoFunction(signature, emptyList(), funcName, emptyList(), packageName = "", emptyList(), emptyList()) val mockSort = ctx.typeToSort(returnType) val mockValue = scope.calcOnState { memory.mocker.call( diff --git a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt index 4a51c5d9e9..aaeffc8b8a 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -107,12 +107,11 @@ class GoInstVisitor( 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) + val parameters = inst.args.map { it.accept(exprVisitor) } scope.doWithState { - data.addDeferredCall(lastEnteredMethod, call) + val call = createCall(method, applicationGraph.entryPoints(method).first(), parameters) + data.addDeferredCall(call) } return next(inst) } @@ -123,12 +122,8 @@ class GoInstVisitor( 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 nonNil = ctx.mkNot(ctx.mkHeapRefEq(pointer, ctx.nullRef)) + scope.fork(nonNil, blockOnFalseState = { panic("null pointer dereference") }) ?: throw GoStepAbort() val rvalue = inst.rhv.accept(exprVisitor) scope.doWithState { store(pointer, copyValue(rvalue, inst.rhv.type)) @@ -162,7 +157,8 @@ class GoInstVisitor( val keyIsNew = ctx.mkNot(memory.read(mapContainsLValue)) - memory.write(mapEntryLValue, value.asExpr(value.sort), ctx.trueExpr) + val copiedValue = copyValue(value, inst.value.type) + memory.write(mapEntryLValue, copiedValue.asExpr(value.sort), ctx.trueExpr) memory.write(mapContainsLValue, ctx.trueExpr, ctx.trueExpr) val updatedSize = ctx.mkSizeAddExpr(currentSize, ctx.mkSizeExpr(1)) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt index 9158fd9ad1..5f5d8896f9 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt @@ -38,7 +38,7 @@ class GoMachine( override val options: UMachineOptions, private val customOptions: GoMachineOptions, ) : UMachine() { - private val typeSystem = GoTypeSystem(options.typeOperationsTimeout, program.types.values) + private val typeSystem = GoTypeSystem(options.typeOperationsTimeout, program.types.values, program.methodSets) private val applicationGraph = GoApplicationGraph() private val components = GoComponents(typeSystem, options) private val ctx = GoContext(components, intSize = program.intSize) diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt index 261c64ece8..535d396120 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt @@ -22,6 +22,7 @@ import org.usvm.forkblacklists.UForkBlackList import org.usvm.solver.USatResult import org.usvm.state.GoFlowStatus import org.usvm.state.GoState +import org.usvm.state.advanceArrayValueCopy import org.usvm.statistics.ApplicationGraph import org.usvm.targets.UTargetsSet @@ -60,11 +61,9 @@ class GoInterpreter( } 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] } @@ -78,6 +77,11 @@ class GoInterpreter( override fun step(state: GoState): StepResult { val inst = state.currentStatement val scope = GoStepScope(state, forkBlackList) + if (state.data.pendingArrayCopy != null) { + advanceArrayValueCopy(scope) + return scope.stepResult() + } + val exprVisitor = GoExprVisitor(ctx, program, scope, applicationGraph) val instVisitor = GoInstVisitor(ctx, program, scope, exprVisitor, applicationGraph) @@ -104,7 +108,7 @@ class GoInterpreter( inst.accept(instVisitor) } GoFlowStatus.DEFER -> { - val deferred = state.data.getDeferredCalls(method) + val deferred = state.data.getDeferredCalls() if (deferred.isEmpty()) { state.data.flowStack.removeLast() return next(state, inst, instVisitor) @@ -124,7 +128,7 @@ class GoInterpreter( ) { "Recovered function has no recovery block" }.instructions.first() } - if (state.data.getDeferredCalls(method).isEmpty()) { + if (state.data.getDeferredCalls().isEmpty()) { state.handlePanic() return GoNullInst(method) } 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 8bcdd44a25..5bbb1f66ee 100644 --- a/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt @@ -139,7 +139,16 @@ class Converter private constructor() { val globals = pkg.members.filterIsInstance().map { global -> GoGlobal(global.index, global.name, getType(global.goType)) } - return GoPackage(pkg.name, methods, globals, typesMap.toMap(), intSize = pkg.intSize) + val methodSets = pkg.types.values.mapNotNull { type -> + val declaredMethods = when (type) { + is Type.Named -> type.methods + is Type.Pointer -> type.methods + is Type.Interface -> type.methods + else -> return@mapNotNull null + } + getType(type.name) to declaredMethods + }.toMap() + return GoPackage(pkg.name, methods, globals, typesMap.toMap(), intSize = pkg.intSize, methodSets = methodSets) } private fun unpack(index: Int, param: Value): GoParameter { @@ -735,7 +744,7 @@ class Converter private constructor() { private fun getType(type: String): GoType = typesMap.getValue(type) private fun functionAlias(name: String): GoFunction { - return GoFunction(OpaqueType(name), emptyList(), name, emptyList(), "", emptyList(), emptyList()) + return GoFunction(OpaqueType(name), emptyList(), name, emptyList(), packageName = "", emptyList(), emptyList()) } private fun unsupportedInstruction(parent: GoMethod): GoInst { 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 125026b614..b289942111 100644 --- a/usvm-go/src/main/kotlin/org/usvm/model/Model.kt +++ b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt @@ -599,6 +599,7 @@ sealed interface Type { data class Pointer( override val name: String, val elem: String, + val methods: List = emptyList(), ) : Type @Serializable diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoArrayValueCopy.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoArrayValueCopy.kt new file mode 100644 index 0000000000..b8e0ca9844 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoArrayValueCopy.kt @@ -0,0 +1,75 @@ +package org.usvm.state + +import io.ksmt.utils.asExpr +import org.jacodb.go.api.GoType +import org.usvm.UExpr +import org.usvm.USizeSort +import org.usvm.USort +import org.usvm.interpreter.GoStepScope +import org.usvm.memory.GoArrayView +import org.usvm.memory.readGoArrayIndex +import org.usvm.memory.writeGoArrayIndex +import org.usvm.mkSizeAddExpr +import org.usvm.mkSizeExpr +import org.usvm.mkSizeLtExpr + +internal data class GoArrayValueCopy( + val source: GoArrayView, + val destination: GoArrayView, + val length: UExpr, + val elementType: GoType, +) + +internal data class GoArrayCopyOperation( + val sourceState: GoState, + val copies: List, + val result: UExpr, + val copyIndex: Int = 0, + val elementIndex: Int = 0, +) + +// Perform one element per machine step so symbolic lengths remain subject to normal analysis budgets. +internal fun advanceArrayValueCopy(scope: GoStepScope) { + val operation = scope.calcOnState { checkNotNull(data.pendingArrayCopy) } + val copy = operation.copies.getOrNull(operation.copyIndex) + if (copy == null) { + scope.doWithState { + data.pendingArrayCopy = null + data.builtinResult = operation.result + } + return + } + + val ctx = operation.sourceState.ctx + val index = ctx.mkSizeExpr(operation.elementIndex) + val hasElement = ctx.mkSizeLtExpr(index, copy.length) + scope.fork(hasElement, blockOnFalseState = { + data.pendingArrayCopy = operation.copy(copyIndex = operation.copyIndex + 1, elementIndex = 0) + }) ?: return + + scope.doWithState { + val sourceIndex = ctx.mkSizeAddExpr(copy.source.offset, index) + val sourceValue = operation.sourceState.memory.readGoArrayIndex( + copy.source.backing, + sourceIndex, + copy.source.storageType, + ctx.addressSort, + ) + val copied = if (sourceValue == ctx.nullRef) { + sampleValue(copy.elementType) + } else { + copyValue(sourceValue, copy.elementType, source = operation.sourceState) + } + val destinationIndex = ctx.mkSizeAddExpr(copy.destination.offset, index) + + memory.writeGoArrayIndex( + copy.destination.backing, + destinationIndex, + copy.destination.storageType, + ctx.addressSort, + copied.asExpr(ctx.addressSort), + guard = ctx.trueExpr, + ) + data.pendingArrayCopy = operation.copy(elementIndex = operation.elementIndex + 1) + } +} 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 19662f6e09..bf772bb270 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -164,6 +164,7 @@ class GoState( fun returnValue(valueToReturn: UExpr, type: GoType) { val returnFromMethod = lastEnteredMethod val returnSite = callStack.pop() + data.popDeferredFrame() if (callStack.isNotEmpty()) { memory.stack.pop() } @@ -183,6 +184,7 @@ class GoState( require(methodResult is GoMethodResult.Panic) val returnSite = callStack.pop() + data.popDeferredFrame() if (callStack.isNotEmpty()) { memory.stack.pop() } @@ -212,13 +214,16 @@ class GoState( data.flowStack.add(GoFlowStatus.DEFER) } + fun createCall(method: GoMethod, entrypoint: GoInst, arguments: List>): GoCall { + val copiedArguments = arguments.mapIndexed { index, value -> + copyValue(value, method.parameters[index].type as GoType) + } + val freeVariables = (method as? GoFunction)?.freeVars.orEmpty().map(::findParam) + return GoCall(method, entrypoint, copiedArguments, freeVariables) + } + 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) @@ -227,10 +232,11 @@ class GoState( data.flowStack.add(GoFlowStatus.NORMAL) callStack.push(call.method, returnInst) - if (methodInfo.arguments.isEmpty()) { + data.pushDeferredFrame() + if (call.arguments == null) { memory.stack.push(methodInfo.argumentsCount, methodInfo.variablesCount) } else { - memory.stack.push(methodInfo.arguments, methodInfo.variablesCount) + memory.stack.push(call.arguments.toTypedArray(), methodInfo.variablesCount) } parameters.forEachIndexed { i, parameter -> @@ -246,13 +252,15 @@ class GoState( } } - parameters.forEachIndexed { index, parameter -> - val type = parameter.type as GoType - val lvalue = URegisterStackLValue(typeToSort(type), index) - memory.write(lvalue, copyValue(memory.read(lvalue), type).asExpr(lvalue.sort), trueExpr) + if (call.arguments == null) { + parameters.forEachIndexed { index, parameter -> + val type = parameter.type as GoType + val lvalue = URegisterStackLValue(typeToSort(type), index) + memory.write(lvalue, copyValue(memory.read(lvalue), type).asExpr(lvalue.sort), trueExpr) + } } - freeVariables.forEachIndexed { i, variable -> + call.freeVariables.forEachIndexed { i, variable -> val lvalue = URegisterStackLValue(variable.sort, i + freeVariableOffset(call.method)) memory.write(lvalue, variable.asExpr(variable.sort), trueExpr) } @@ -440,7 +448,6 @@ class GoState( registers.pop() } - error("param not found") } 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 a9ef8a6a4b..31bea6cc79 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt @@ -1,9 +1,10 @@ package org.usvm.state -import org.jacodb.go.api.GoMethod import org.jacodb.go.api.GoType import org.usvm.GoCall +import org.usvm.UExpr import org.usvm.UHeapRef +import org.usvm.USort import org.usvm.memory.GoArrayView import org.usvm.memory.ULValue @@ -22,30 +23,44 @@ class GoStateData( internal val pointerConversions: MutableMap = hashMapOf() - private val deferredCalls: MutableMap> = hashMapOf() + internal var pendingArrayCopy: GoArrayCopyOperation? = null + internal var builtinResult: UExpr? = null + + private val deferredCalls: ArrayDeque> = ArrayDeque() val flowStatus: GoFlowStatus get() = flowStack.last() - fun getDeferredCalls(method: GoMethod): ArrayDeque = deferredCalls[method] ?: ArrayDeque() + fun pushDeferredFrame() { + deferredCalls.addLast(ArrayDeque()) + } - fun addDeferredCall(method: GoMethod, call: GoCall) { - deferredCalls.getOrPut(method) { ArrayDeque() }.addLast(call) + fun popDeferredFrame() { + deferredCalls.removeLast() + } + + fun getDeferredCalls(): ArrayDeque = deferredCalls.last() + + fun addDeferredCall(call: GoCall) { + getDeferredCalls().addLast(call) } fun clone(): GoStateData = GoStateData(clonedFlowStack()).also { it.pointerTargets.putAll(pointerTargets) it.pointerConversions.putAll(pointerConversions) it.arrayViews.putAll(arrayViews) - for ((method, calls) in deferredCalls) { - calls.forEach { call -> it.addDeferredCall(method, call) } - } + it.pendingArrayCopy = pendingArrayCopy + it.builtinResult = builtinResult + deferredCalls.forEach { calls -> it.deferredCalls.addLast(ArrayDeque(calls)) } } fun mergeWith(other: GoStateData): GoStateData? { - val thisCalls = deferredCalls.mapValues { it.value.toList() } - val otherCalls = other.deferredCalls.mapValues { it.value.toList() } + val thisCalls = deferredCalls.map { it.toList() } + val otherCalls = other.deferredCalls.map { it.toList() } val sameCalls = thisCalls == otherCalls + val sameOperation = pendingArrayCopy == other.pendingArrayCopy && builtinResult == other.builtinResult + if (!sameOperation) return null + if (flowStack.toList() != other.flowStack.toList() || !sameCalls || arrayViews != other.arrayViews || pointerTargets != other.pointerTargets || pointerConversions != other.pointerConversions diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt index 5d0bac7759..0b8f0cc1da 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt @@ -16,34 +16,38 @@ import org.usvm.mkSizeExpr import org.usvm.sizeSort import org.usvm.type.underlying -internal fun GoState.copyValue(value: UExpr, type: GoType): UExpr = with(ctx) { +internal fun GoState.copyValue( + value: UExpr, + type: GoType, + source: GoState = this, +): UExpr = with(ctx) { val underlying = type.underlying() if (underlying !is StructType && underlying !is ArrayType) return value val reference = value.asExpr(addressSort) if (type is NamedType) { - return box(copyValue(unbox(reference, addressSort), underlying), type) + return box(copyValue(source.unbox(reference, addressSort), underlying, source), type) } return when (underlying) { is StructType -> { val fields = underlying.fields.orEmpty().mapIndexed { index, fieldType -> - copyValue(memory.readField(reference, index, typeToSort(fieldType)), fieldType) + copyValue(source.memory.readField(reference, index, typeToSort(fieldType)), fieldType, source) } mkTuple(type, fields = fields.toTypedArray()) } is ArrayType -> { - val view = arrayView(reference, type) + val view = source.arrayView(reference, type) val elementType = underlying.elementType val sort = typeToSort(elementType) val fields = (0 until underlying.len.toInt()).asSequence().map { index -> - val element = memory.readGoArrayIndex( + val element = source.memory.readGoArrayIndex( view.backing, mkSizeAddExpr(view.offset, mkSizeExpr(index)), view.storageType, sort, ) - copyValue(element, elementType).asExpr(sort) + copyValue(element, elementType, source).asExpr(sort) } memory.allocateGoArrayInitialized(type, sort, sizeSort, fields) } 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 5917d8668d..77ab552478 100644 --- a/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt @@ -17,6 +17,7 @@ import kotlin.time.Duration class GoTypeSystem( override val typeOperationsTimeout: Duration, val types: Collection, + private val methodSets: Map>, ) : UTypeSystem { private val goAnyType = InterfaceType(emptyList(), "any") private val topTypeStream by lazy { USupportTypeStream.from(this, goAnyType) } @@ -60,7 +61,8 @@ class GoTypeSystem( } override fun isSupertype(supertype: GoType, type: GoType): Boolean = when { - supertype == type -> { + // JacoDB pointer type names omit "*", so equal names alone do not establish Go type identity. + supertype::class == type::class && supertype == type -> { true } supertype is NamedType && supertype.underlyingType is InterfaceType -> { @@ -82,10 +84,10 @@ class GoTypeSystem( true } impl is NamedType -> { - impl.methods.containsAll(iface.methods) + methodSets.getValue(impl).containsAll(iface.methods) } impl is PointerType -> { - impl.baseType.underlying() !is InterfaceType && implements(iface, impl.baseType) + impl.baseType.underlying() !is InterfaceType && methodSets.getValue(impl).containsAll(iface.methods) } else -> { false diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt index 148eee4204..a1060bc1ca 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt @@ -1,6 +1,5 @@ package org.usvm.samples -import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull @@ -16,10 +15,9 @@ import mu.KotlinLogging import org.junit.jupiter.api.Test import org.usvm.generatedGoFile import org.usvm.interpreter.GoPointer -import java.nio.file.Files -import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds class GoExamplesReplayTest : GoMethodTestRunner() { @Test @@ -46,7 +44,7 @@ internal object GoExamplesReplay { private val logger = KotlinLogging.logger {} fun replay(method: String, executions: List) { - assertTrue(executions.isNotEmpty(), "Replay needs generated inputs") + assertTrue(executions.isNotEmpty(), message = "Replay needs generated inputs") val request = buildJsonObject { put("method", method) put( @@ -55,50 +53,35 @@ internal object GoExamplesReplay { ) } - val replayFile = Files.createTempFile("usvm-go-examples-replay-", ".json").toFile() - val logFile = Files.createTempFile("usvm-go-examples-replay-", ".log").toFile() - try { - replayFile.writeText(request.toString()) - val builder = ProcessBuilder( - generatedGoFile("native-examples-replay.test").path, - "-test.run=^TestReplayExamples$" - ).redirectErrorStream(true).redirectOutput(logFile) - builder.environment()["USVM_GO_REPLAY_FILE"] = replayFile.path - val process = builder.start() - try { - assertTrue(process.waitFor(10, TimeUnit.SECONDS), "Native replay timed out") - assertEquals(expected = 0, actual = process.exitValue(), message = logFile.readText()) - } finally { - process.destroyForcibly() - } + val nativeResults = replayWithNativeGo( + executable = generatedGoFile("native-examples-replay.test"), + testName = "TestReplayExamples", + request = request, + timeout = 10.seconds, + ).jsonArray - val nativeResults = Json.parseToJsonElement(replayFile.readText()).jsonArray - assertEquals(executions.size, nativeResults.size) - executions.zip(nativeResults).forEachIndexed { index, (execution, native) -> - val result = native.jsonObject - assertEquals( - expected = execution.result.isPanic, - actual = result.getValue("isPanic").jsonPrimitive.boolean, - message = "$method witness $index panic" - ) - if (execution.result.isSuccess) { - assertEquals( - expected = toJson(execution.result.value), - actual = result.getValue("value"), - message = "$method witness $index result" - ) - } + assertEquals(executions.size, nativeResults.size) + executions.zip(nativeResults).forEachIndexed { index, (execution, native) -> + val result = native.jsonObject + assertEquals( + expected = execution.result.isPanic, + actual = result.getValue("isPanic").jsonPrimitive.boolean, + message = "$method witness $index panic" + ) + if (execution.result.isSuccess) { assertEquals( - expected = toJson(execution.argumentsAfter), - actual = result.getValue("argumentsAfter"), - message = "$method witness $index arguments after" + expected = toJson(execution.result.value), + actual = result.getValue("value"), + message = "$method witness $index result" ) } - logger.info { "Replayed ${executions.size} generated inputs for $method with native Go" } - } finally { - replayFile.delete() - logFile.delete() + assertEquals( + expected = toJson(execution.argumentsAfter), + actual = result.getValue("argumentsAfter"), + message = "$method witness $index arguments after" + ) } + logger.info { "Replayed ${executions.size} generated inputs for $method with native Go" } } private fun toJson(value: Any?): JsonElement = when (value) { diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeReplay.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeReplay.kt new file mode 100644 index 0000000000..7732739b7b --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeReplay.kt @@ -0,0 +1,42 @@ +package org.usvm.samples + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import java.io.File +import java.nio.file.Files +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration + +internal fun replayWithNativeGo( + executable: File, + testName: String, + request: JsonElement, + timeout: Duration, + environment: Map = emptyMap(), +): JsonElement { + val replayFile = Files.createTempFile("usvm-go-replay-", ".json").toFile() + val logFile = Files.createTempFile("usvm-go-replay-", ".log").toFile() + try { + replayFile.writeText(request.toString()) + val builder = ProcessBuilder(executable.path, "-test.run=^$testName$") + .redirectErrorStream(true) + .redirectOutput(logFile) + builder.environment().putAll(environment) + builder.environment()["USVM_GO_REPLAY_FILE"] = replayFile.path + val process = builder.start() + try { + val completed = process.waitFor(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS) + assertTrue(completed, message = "Native replay timed out: $testName") + assertEquals(expected = 0, actual = process.exitValue(), message = logFile.readText()) + } finally { + process.destroyForcibly() + } + + return Json.parseToJsonElement(replayFile.readText()) + } finally { + replayFile.delete() + logFile.delete() + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt index 18dc398300..5d5c852796 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt @@ -1,16 +1,17 @@ package org.usvm.samples import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long import org.usvm.generatedGoFile import org.usvm.test.util.checkers.eq -import java.nio.file.Files -import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds open class GoNativeTestRunner : GoMethodTestRunner(fixture = "regressions") { protected fun checkNative(method: String) { @@ -29,37 +30,22 @@ open class GoNativeTestRunner : GoMethodTestRunner(fixture = "regressions") { protected fun replayInputs(methodName: String, expectedExecutions: Int) { val executions = runner(methodName, options) - assertTrue(executions.all { it.result.isSuccess }, "Native replay expects successful executions") + assertTrue(executions.all { it.result.isSuccess }, message = "Native replay expects successful executions") assertEquals(expectedExecutions, executions.size, message = "One witness per branch") val inputs = executions.map { (it.arguments.single() as Number).toLong() } if (methodName == "symbolicBranch") { assertTrue(inputs.any { it < 0 } && inputs.any { it == 0L } && inputs.any { it > 0 }) } - val replayFile = Files.createTempFile("usvm-go-replay-", ".json").toFile() - val logFile = Files.createTempFile("usvm-go-replay-", ".log").toFile() - try { - replayFile.writeText(inputs.joinToString(prefix = "[", postfix = "]")) - val builder = ProcessBuilder( - generatedGoFile("native-replay.test").path, - "-test.run=^TestReplaySymbolicInputs$" - ) - .redirectErrorStream(true).redirectOutput(logFile) - builder.environment()["USVM_GO_REPLAY_FILE"] = replayFile.path - builder.environment()["USVM_GO_REPLAY_METHOD"] = methodName - val process = builder.start() - try { - assertTrue(process.waitFor(5, TimeUnit.SECONDS), "Native replay timed out") - assertEquals(expected = 0, actual = process.exitValue(), message = logFile.readText()) - } finally { - process.destroyForcibly() - } - val nativeOutputs = Json.parseToJsonElement(replayFile.readText()).jsonArray.map { it.jsonPrimitive.long } - val symbolicOutputs = executions.map { (it.result.value as Number).toLong() } - assertEquals(nativeOutputs, symbolicOutputs, "Replay of generated inputs") - } finally { - replayFile.delete() - logFile.delete() - } + val nativeOutputs = replayWithNativeGo( + executable = generatedGoFile("native-replay.test"), + testName = "TestReplaySymbolicInputs", + request = JsonArray(inputs.map(::JsonPrimitive)), + timeout = 5.seconds, + environment = mapOf("USVM_GO_REPLAY_METHOD" to methodName), + ).jsonArray.map { it.jsonPrimitive.long } + val symbolicOutputs = executions.map { (it.result.value as Number).toLong() } + + assertEquals(nativeOutputs, symbolicOutputs, message = "Replay of generated inputs") } } diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt index 06732e5123..dbe0a27ba6 100644 --- a/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt @@ -19,21 +19,33 @@ class SampleCoverageTest { val expected = pkg.methods.filter { '$' !in it.metName }.map { it.metName }.toSet() val registered = samples.filter { it.fixture == fixture }.map { it.method } - assertEquals(expected, registered.toSet(), "Every $fixture method must have a semantic test") - assertEquals(registered.toSet().size, registered.size, "Each $fixture method must be registered once") + assertEquals(expected, registered.toSet(), message = "Every $fixture method must have a semantic test") + assertEquals( + registered.toSet().size, + registered.size, + message = "Each $fixture method must be registered once" + ) } } @Test fun everyConstantRegressionHasANativeOracle() { val pkg = Converter.unpack(Parser().deserialize(generatedGoFile("regressions/usvm_regressions.json").path)) - val expected = pkg.methods.filter { it.parameters.isEmpty() && it.metName != "init" }.map { it.metName }.toSet() + // SSA closure bodies are exercised through their enclosing functions. + val expected = pkg.methods.filter { + it.parameters.isEmpty() && it.metName != "init" && '$' !in it.metName + }.map { it.metName }.toSet() val oracle = Json.parseToJsonElement(generatedGoFile("native-oracle.json").readText()).jsonObject assertEquals(expected, oracle.keys) } private val sampleClasses = listOf( + org.usvm.samples.exceptions.DeferRegressionTest::class.java, + org.usvm.samples.types.InterfaceRegressionTest::class.java, + org.usvm.samples.collections.maps.MapValueRegressionTest::class.java, + org.usvm.samples.collections.slices.CompositeSliceRegressionTest::class.java, + org.usvm.samples.pointers.NamedPointerRegressionTest::class.java, org.usvm.samples.algorithms.AlgorithmsTest::class.java, org.usvm.samples.algorithms.SlowAlgorithmsTest::class.java, org.usvm.samples.arithmetic.ArithmeticRegressionTest::class.java, diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapValueRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapValueRegressionTest.kt new file mode 100644 index 0000000000..b1e62b0e71 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapValueRegressionTest.kt @@ -0,0 +1,39 @@ +package org.usvm.samples.collections.maps + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class MapValueRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "mapStructCopy", fixture = "regressions") + fun mapStructCopy() = checkNative(method = "mapStructCopy") + + @Test + @GoSample(method = "mapArrayCopy", fixture = "regressions") + fun mapArrayCopy() = checkNative(method = "mapArrayCopy") + + @Test + @GoSample(method = "mapLookupStructCopy", fixture = "regressions") + fun mapLookupStructCopy() = checkNative(method = "mapLookupStructCopy") + + @Test + @GoSample(method = "missingStructLookup", fixture = "regressions") + fun missingStructLookup() = checkNative(method = "missingStructLookup") + + @Test + @GoSample(method = "missingNamedLookup", fixture = "regressions") + fun missingNamedLookup() = checkNative(method = "missingNamedLookup") + + @Test + @GoSample(method = "missingArrayLookup", fixture = "regressions") + fun missingArrayLookup() = checkNative(method = "missingArrayLookup") + + @Test + @GoSample(method = "missingStructLookupComma", fixture = "regressions") + fun missingStructLookupComma() = checkNative(method = "missingStructLookupComma") + + @Test + @GoSample(method = "namedMapAssertionZero", fixture = "regressions") + fun namedMapAssertionZero() = checkNative(method = "namedMapAssertionZero") +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/CompositeSliceRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/CompositeSliceRegressionTest.kt new file mode 100644 index 0000000000..e24d90e51e --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/slices/CompositeSliceRegressionTest.kt @@ -0,0 +1,93 @@ +package org.usvm.samples.collections.slices + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.eq +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class CompositeSliceRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "compositePointerFieldsRemainShared", fixture = "regressions") + fun compositePointerFieldsRemainShared() = checkNative(method = "compositePointerFieldsRemainShared") + + @Test + @GoSample(method = "sliceStructCopy", fixture = "regressions") + fun sliceStructCopy() = checkNative(method = "sliceStructCopy") + + @Test + @GoSample(method = "appendStructCopy", fixture = "regressions") + fun appendStructCopy() = checkNative(method = "appendStructCopy") + + @Test + @GoSample(method = "sliceArrayCopy", fixture = "regressions") + fun sliceArrayCopy() = checkNative(method = "sliceArrayCopy") + + @Test + @GoSample(method = "appendArrayCopy", fixture = "regressions") + fun appendArrayCopy() = checkNative(method = "appendArrayCopy") + + @Test + @GoSample(method = "overlapCompositeCopy", fixture = "regressions") + fun overlapCompositeCopy() = checkNative(method = "overlapCompositeCopy") + + @Test + @GoSample(method = "appendCompositeReuse", fixture = "regressions") + fun appendCompositeReuse() = checkNative(method = "appendCompositeReuse") + + @Test + @GoSample(method = "appendCompositeAllocate", fixture = "regressions") + fun appendCompositeAllocate() = checkNative(method = "appendCompositeAllocate") + + @Test + @GoSample(method = "narrowSignedIndex", fixture = "regressions") + fun narrowSignedIndex() = checkNative(method = "narrowSignedIndex") + + @Test + @GoSample(method = "negativeInt16Index", fixture = "regressions") + fun negativeInt16Index() = checkNative(method = "negativeInt16Index") + + @Test + @GoSample(method = "namedSliceAssertionZero", fixture = "regressions") + fun namedSliceAssertionZero() = checkNative(method = "namedSliceAssertionZero") + + @Test + @GoSample(method = "symbolicNarrowIndex", fixture = "regressions") + fun narrowSignedInputsRequireNonnegativeIndices() { + checkMatches( + method = "symbolicNarrowIndex", + analysisResultsNumberMatcher = eq(count = 2), + { index: Number, result: GoResult -> index.toLong() < 0 && result.isPanic }, + { index: Number, result: GoResult -> index.toLong() >= 0 && result.long == 0L }, + ) + } + + @Test + @GoSample(method = "symbolicCompositeCopy", fixture = "regressions") + fun copyPreservesValuesWithSymbolicLengths() { + checkDiscoveredProperties( + method = "symbolicCompositeCopy", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { length: Number, result: GoResult -> length.toLong() !in 0..3 && result.long == -1L }, + { length: Number, result: GoResult -> + length.toLong() in 0..3 && result.long == length.toLong() * 10 + 1 + }, + ) + replayInputs(methodName = "symbolicCompositeCopy", expectedExecutions = 6) + } + + @Test + @GoSample(method = "symbolicCompositeAppend", fixture = "regressions") + fun appendPreservesValuesWithSymbolicLengths() { + checkDiscoveredProperties( + method = "symbolicCompositeAppend", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { length: Number, result: GoResult -> length.toLong() !in 0..3 && result.long == -1L }, + { length: Number, result: GoResult -> + length.toLong() in 0..3 && result.long == length.toLong() * 10 + 1 + }, + ) + replayInputs(methodName = "symbolicCompositeAppend", expectedExecutions = 6) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferRegressionTest.kt new file mode 100644 index 0000000000..e204fe99f2 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/exceptions/DeferRegressionTest.kt @@ -0,0 +1,50 @@ +package org.usvm.samples.exceptions + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoPointer +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class DeferRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "deferredArguments", fixture = "regressions") + fun deferredArguments() = checkNative(method = "deferredArguments") + + @Test + @GoSample(method = "repeatedDeferArguments", fixture = "regressions") + fun repeatedDeferArguments() = checkNative(method = "repeatedDeferArguments") + + @Test + @GoSample(method = "deferredStructArgument", fixture = "regressions") + fun deferredStructArgument() = checkNative(method = "deferredStructArgument") + + @Test + @GoSample(method = "recursiveDeferredArguments", fixture = "regressions") + fun recursiveDeferredArguments() = checkNative(method = "recursiveDeferredArguments") + + @Test + @GoSample(method = "reviewDeferSet", fixture = "regressions") + fun deferredHelperMutatesThePointedValue() { + checkParameterMutations( + method = "reviewDeferSet", + { execution -> execution.arguments[0] == null && execution.result.isPanic }, + { execution -> + val pointer = execution.argumentsAfter[0] as? GoPointer + execution.result.isSuccess && pointer?.value == execution.arguments[1] + }, + ) + } + + @Test + @GoSample(method = "recursiveDeferFrames", fixture = "regressions") + fun recursionKeepsSeparateDeferredFrames() { + checkDiscoveredProperties( + method = "recursiveDeferFrames", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { depth: Number, result: GoResult -> depth.toLong() !in 0..2 && result.long == -1L }, + { depth: Number, result: GoResult -> depth.toLong() in 0..2 && result.long == depth.toLong() + 1 }, + ) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/pointers/NamedPointerRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/pointers/NamedPointerRegressionTest.kt new file mode 100644 index 0000000000..a36e7fc3c7 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/pointers/NamedPointerRegressionTest.kt @@ -0,0 +1,11 @@ +package org.usvm.samples.pointers + +import org.junit.jupiter.api.Test +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoSample + +class NamedPointerRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "namedPointerAssertionZero", fixture = "regressions") + fun namedPointerAssertionZero() = checkNative(method = "namedPointerAssertionZero") +} diff --git a/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfaceRegressionTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfaceRegressionTest.kt new file mode 100644 index 0000000000..52aab674de --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfaceRegressionTest.kt @@ -0,0 +1,70 @@ +package org.usvm.samples.types + +import org.junit.jupiter.api.Test +import org.usvm.interpreter.GoInterfaceValue +import org.usvm.interpreter.GoPointer +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.GoStruct +import org.usvm.samples.field +import org.usvm.samples.longValue +import org.usvm.test.util.checkers.ignoreNumberOfAnalysisResults + +class InterfaceRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "interfaceAssertion", fixture = "regressions") + fun interfaceAssertion() = checkNative(method = "interfaceAssertion") + + @Test + @GoSample(method = "interfaceAssertionNoComma", fixture = "regressions") + fun interfaceAssertionNoComma() = checkNative(method = "interfaceAssertionNoComma") + + @Test + @GoSample(method = "pointerInterfaceCall", fixture = "regressions") + fun pointerInterfaceCall() = checkNative(method = "pointerInterfaceCall") + + @Test + @GoSample(method = "valueHasPointerMethods", fixture = "regressions") + fun valueHasPointerMethods() = checkNative(method = "valueHasPointerMethods") + + @Test + @GoSample(method = "(usvm/regressions.reviewError).Error", fixture = "regressions") + fun valueReceiverReturnsItsString() { + checkDiscoveredProperties( + method = "(usvm/regressions.reviewError).Error", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { value: String, result: GoResult -> result.isSuccess && result.value == value }, + ) + } + + @Test + @GoSample(method = "(*usvm/regressions.reviewCounter).Read", fixture = "regressions") + fun pointerReceiverReadsItsField() { + checkDiscoveredProperties( + method = "(*usvm/regressions.reviewCounter).Read", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { counter: GoPointer?, result: GoResult -> counter == null && result.isPanic }, + { counter: GoPointer?, result: GoResult -> + counter != null && result.long == (counter.value as GoStruct).field(index = 0).longValue() + }, + ) + } + + @Test + @GoSample(method = "symbolicInterfaceReceiver", fixture = "regressions") + fun interfaceInputsUseThePointerMethodSet() { + checkDiscoveredProperties( + method = "symbolicInterfaceReceiver", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { reader: GoInterfaceValue?, result: GoResult -> reader == null && result.isPanic }, + { reader: GoInterfaceValue?, result: GoResult -> + reader != null && reader.value == null && result.isPanic + }, + { reader: GoInterfaceValue?, result: GoResult -> + val pointer = reader?.value as? GoPointer + pointer != null && result.long == (pointer.value as GoStruct).field(index = 0).longValue() + }, + ) + } +}