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..b8978b001a --- /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(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) + 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..a910bdcd8f --- /dev/null +++ b/usvm-go/README.md @@ -0,0 +1,160 @@ +# 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. + +## Test organization and symbolic expectations + +Tests follow the Java frontend's `TestRunner` pattern under `org.usvm.samples`: + +- `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 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 +`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: + +```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. +`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 **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. + +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 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, 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. + 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`; 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; +they are not included in the default suite. + +## Representation and boundaries + +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 +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. +- 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/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. +- Selected array/struct copies and scalar-pointer conversions have native validation. + 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/build.gradle.kts b/usvm-go/build.gradle.kts new file mode 100644 index 0000000000..a8c131c39f --- /dev/null +++ b/usvm-go/build.gradle.kts @@ -0,0 +1,86 @@ +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") +} + +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, compileGoExamplesReplay) +} + +tasks.named("manualTest") { + dependsOn(generateGoIr, compileGoExamplesReplay) +} + +tasks.withType().configureEach { + ignoreFailures = false + setExcludes(listOf("**/resources/**", "**/build/**", "**/generated/**")) +} 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/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/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..2992d4f69c --- /dev/null +++ b/usvm-go/src/main/go/model.go @@ -0,0 +1,497 @@ +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"` + Methods []string `yaml:"methods,omitempty" json:"methods,omitempty"` +} + +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..994d3d4548 --- /dev/null +++ b/usvm-go/src/main/go/pack.go @@ -0,0 +1,730 @@ +package main + +import ( + "encoding/base64" + "go/constant" + "go/types" + "log" + "sort" + "strconv" + "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()) + 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: + 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(), 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(), 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 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())) + } + + 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(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 + } + return result +} 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.go b/usvm-go/src/main/go/regressions/semantics.go new file mode 100644 index 0000000000..a361f48753 --- /dev/null +++ b/usvm-go/src/main/go/regressions/semantics.go @@ -0,0 +1,546 @@ +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 +} + +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 +} + +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) +} + +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 new file mode 100644 index 0000000000..1a7802fcc2 --- /dev/null +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -0,0 +1,188 @@ +package regressions + +import ( + "encoding/json" + "fmt" + "os" + "testing" +) + +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() }, + "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)) + + 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) + case "symbolicCompositeCopy": + outputs[index] = symbolicCompositeCopy(input) + case "symbolicCompositeAppend": + outputs[index] = symbolicCompositeAppend(input) + case "symbolicNamedInterfaceRoundTrip": + outputs[index] = int(symbolicNamedInterfaceRoundTrip(namedNumber(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..d5a85d6847 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt @@ -0,0 +1,133 @@ +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): GoMethodInfo = methodInfo.getOrPut(method) { + val localsCount = method.blocks.flatMap { it.instructions }.filterIsInstance().size + GoMethodInfo( + variablesCount = localsCount + getFreeVariablesCount(method), + argumentsCount = method.parameters.size, + ) + } + + 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 getArgsCount(method: GoMethod): Int = getMethodInfo(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..f40248b41e --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt @@ -0,0 +1,77 @@ +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, +) { + override fun toString(): String { + return "variables: $variablesCount, arguments: $argumentsCount" + } +} + +data class GoCall( + val method: GoMethod, + val entrypoint: GoInst, + val arguments: List>? = null, + val freeVariables: List> = emptyList(), +) + +class GoPackage( + val name: String, + val methods: List, + val globals: List, + val types: Map, + val intSize: Int, + val methodSets: Map> = emptyMap(), +) { + 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 } + val methodSets = packages.flatMap { it.methodSets.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..768238fc32 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -0,0 +1,1540 @@ +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 +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.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.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.GoArrayCopyOperation +import org.usvm.state.GoArrayValueCopy +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 +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 + } + + if (expr.callee != null) return callInterface(expr, func) + + val args = expr.args + val method = when { + 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) } + parameters.forEachIndexed { index, value -> + val parameterType = method.parameters[index].type as GoType + scope.assert(scope.calcOnState { valueShape(value, parameterType) }) ?: throw GoStepAbort() + } + scope.doWithState { + val call = createCall(method, applicationGraph.entryPoints(method).first(), parameters) + addCall(call, currentStatement) + } + 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 { + val call = createCall(method, applicationGraph.entryPoints(method).first(), listOf(value) + args) + addCall(call, currentStatement) + } + } + + scope.forkMulti(cases) + 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 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) + } + + 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 type = expr.value.type + val operand = expr.value.accept(this) + 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() + } + + 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() + checkSliceBound(length, capacity) ?: 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() + 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) + 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 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) + return mkPointer(fieldType, fieldLValue) + } + + override fun visitGoFieldExpr(expr: GoFieldExpr): UExpr { + 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)) + } + } + + 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 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) + + 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 value = ctx.mkIte( + contains, + trueBranch = { scope.calcOnState { copyValue(memory.read(lvalue), mapType.valueType).asExpr(valueSort) } }, + falseBranch = { scope.calcOnState { sampleValue(mapType.valueType).asExpr(valueSort) } } + ) + + return scope.calcOnState { + if (commaOk) { + mkTuple(TupleType(listOf(mapType.valueType, GoBasicTypes.BOOL)), value, contains) + } else { + value + } + } + } + + 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(unboxNamedRef(it, expr.instance.type), 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 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 zero = scope.calcOnState { sampleValue(type) } + val isNil = ctx.mkHeapRefEq(reference, ctx.nullRef) + val boxed = ctx.mkIte(isNil, trueBranch = { box(zero, type) }, falseBranch = { reference }) + scope.assert(isBoxed(boxed)) ?: throw GoStepAbort() + + val matches = scope.calcOnState { + 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 + } else { + 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 result = ctx.mkIte(matches, value.asExpr(value.sort), zero.asExpr(value.sort)) + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 || type.underlying() is InterfaceType) { + return expr + } + + 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 } + ) + + 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) { + 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 + val pointerOperands = expr.lhv.type.underlying() is PointerType || expr.rhv.type.underlying() is PointerType + if (isEquality && bothReferences && (hasNilOperand || pointerOperands)) { + 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("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) { + 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 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> { + 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 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) + 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) + scope.assert(ctx.mkOr(ctx.mkHeapRefEq(pointer, nullRef), isPointer(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 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 || type.underlying() is InterfaceType) { + return expr + } + + val sort = ctx.typeToSort(type.underlying()) + if (sort == ctx.addressSort) { + return unboxNamedRef(expr.asExpr(ctx.addressSort), type) + } + ensureNamedScalar(expr, type) + + return scope.calcOnState { unbox(expr.asExpr(ctx.addressSort), sort) } + } + + private fun tryBox(expr: UExpr, targetType: GoType): UExpr { + 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 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") + }) + } + + 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 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 { + val completed = scope.calcOnState { + data.builtinResult?.also { data.builtinResult = null } + } + if (completed != null) return tryBox(completed, returnType) + + return when (method.name) { + "append" -> { + val result = appendArray(args[0], args[1]) + if (result == ctx.noValue) result else tryBox(result, returnType) + } + "copy" -> { + copyArray(args, returnType) + } + + "delete" -> { + 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() + 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 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, + ): UHeapRef = with(ctx) { + val source = mapOrEmpty(srcMap, mapType) + scope.ensureObjectMapSizeCorrect(source, 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, source, mapType, valueSort) + } else { + symbolicPrimitiveMapCopyIntoEmpty(destMap, source, 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 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) + 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) + 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, + 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 -> { + val sourceType = (baseType.underlying() as PointerType).baseType + scope.calcOnState { convertPointer(value.asExpr(ctx.addressSort), sourceType, targetType.baseType) } + } + + 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(), packageName = "", 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..aaeffc8b8a --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -0,0 +1,210 @@ +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.state.copyValue +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) } + + scope.doWithState { + val call = createCall(method, applicationGraph.entryPoints(method).first(), parameters) + data.addDeferredCall(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) + 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)) + } + + 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)) + + 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)) + 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..5f5d8896f9 --- /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, program.methodSets) + 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..535d396120 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt @@ -0,0 +1,145 @@ +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.state.advanceArrayValueCopy +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)) + + 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] + state.addCall(GoCall(method, entrypoint)) + var previousEntrypoint = entrypoint + for (m in program.findInitMethods(method.packageName) + program.findOsInitMethods()) { + state.addCall(GoCall(m, applicationGraph.entryPoints(m).first()), previousEntrypoint) + previousEntrypoint = m.blocks[0].instructions[0] + } + + val model = (solver().check(state.pathConstraints) as USatResult).model + state.models = listOf(model) + + return state + } + + 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) + + 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() + 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().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..2443954134 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -0,0 +1,640 @@ +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.expr.KIteExpr +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.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 +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.collections.immutable.internal.MutabilityOwnership +import org.usvm.constraints.UPathConstraints +import org.usvm.isTrue +import org.usvm.memory.ULValue +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 +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 { + 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") + } + + 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 = mkRegisterReading(idx, sort) + inputScope.convertExpr(expr, type) + } + val inputModel = InputModel(inputValues) + val argumentsAfter = List(method.parameters.size) { index -> + val type = method.parameters[index].type as GoType + 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 = mkRegisterReading(index, typeToSort(type)) + outputScope.convertExpr(original, type) + } + } + + return if (state.isExceptional) { + val panic = state.methodResult as GoMethodResult.Panic + 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, argumentsAfter) + + 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 && type !is InterfaceType) 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 + + 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 (model.eval(string) == model.nullRef) { + return "" + } + + val view = state.arrayView(string, arrayType, sourceMemory = memory) + val lengthUExpr = view.length + val length = resolveLength(lengthUExpr) + + val buffer = ByteBuffer.allocate(length * Byte.SIZE_BYTES) + for (i in 0..? = with(ctx) { + if (model.eval(array) == model.nullRef) { + return null + } + + 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( + 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 (model.eval(slice) == model.nullRef) { + return null + } + + val view = state.arrayView(slice, sliceType, sourceMemory = memory) + val lengthUExpr = view.length + val length = resolveLength(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 (model.eval(map) == model.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.mapNotNull { entry -> + val key = when (entry) { + is URefSetEntryLValue<*> -> { + entry.setElement + } + is USetEntryLValue<*, *, *> -> { + entry.setElement + } + else -> { + error("Inconsistent Go interpreter state") + } + } + + 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 { + UMapEntryLValue(keySort, valueSort, map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) + } + val value = memory.read(lvalue) + convertExpr(key, keyType) to convertExpr(value, valueType) + }.toMap() + ) + } + val getEntries: (UWritableMemory, UHeapRef) -> Set> = { source, ref -> + if (isRefSet) { + source.refSetEntries(ref, mapType) + } else { + source.setEntries(ref, mapType, keySort, USizeExprKeyInfo()) + }.entries + } + + val length = resolveLength(memory.read(UMapLengthLValue(map, mapType, sizeSort))) + + val result = mutableMapOf() + 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 + 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 (model.eval(tuple) == model.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 (model.eval(struct) == model.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 (model.eval(iface) == model.nullRef) { + return null + } + + 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) + } else { + convertExpr(memory.readField(iface, index, typeToSort(type)), type) + } + return GoInterfaceValue(type, value) + } + + fun resolvePointer(pointer: UHeapRef, baseType: GoType): Any? = with(ctx) { + 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 + } + + 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 GoPointer(convertExpr(expr, baseType)) + } + + fun resolveBoxed(value: UHeapRef, type: GoType): Any? = with(ctx) { + if (model.eval(value) == model.nullRef) { + return null + } + + val index = 0 + return convertExpr(memory.readField(value, index, typeToSort(type)), type) + } + } + + 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 + +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?, + val argumentsAfter: List, +) { + 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, + val panicValue: Any?, + val argumentsAfter: List, +) : ProgramExecutionResult { + override fun toString(): String { + return buildString { + appendLine("================================================================") + appendLine("Unsuccessful Execution") + appendLine("----------------------------------------------------------------") + appendLine(inputModel.toString()) + appendLine("----------------------------------------------------------------") + appendLine(panicValue) + 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) + +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/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..cb27a01b79 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayView.kt @@ -0,0 +1,47 @@ +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, + sourceMemory: UReadOnlyMemory = memory, +): GoArrayView { + data.arrayViews[reference]?.let { return it } + + if (reference is KIteExpr) { + 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), + 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 = { 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/model/Converter.kt b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt new file mode 100644 index 0000000000..5bbb1f66ee --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt @@ -0,0 +1,759 @@ +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)) + } + 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 { + 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(), packageName = "", 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..b289942111 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt @@ -0,0 +1,645 @@ +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, + val methods: List = emptyList(), + ) : 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/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/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..bf772bb270 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -0,0 +1,486 @@ +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.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 +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.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() + data.popDeferredFrame() + 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() + data.popDeferredFrame() + 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 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 parameters = mutableListOf().also { + if (call.method is GoFunction) { + it.addAll(call.method.parameters) + } + } + + data.flowStack.add(GoFlowStatus.NORMAL) + callStack.push(call.method, returnInst) + data.pushDeferredFrame() + if (call.arguments == null) { + memory.stack.push(methodInfo.argumentsCount, methodInfo.variablesCount) + } else { + memory.stack.push(call.arguments.toTypedArray(), methodInfo.variablesCount) + } + + parameters.forEachIndexed { i, parameter -> + 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) + } + } + } + + 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) + } + } + + call.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) + ) + } + 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) + } + + 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 + } + 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) + } else { + memory.writeField(pointer, 0, rvalue.sort, rvalue.asExpr(rvalue.sort), guard) + } + } + + 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) + 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( + ref = pointer, + field = 1, + sort = ctx.bv32Sort, + value = ctx.mkBv(POINTER_FIELD, ctx.bv32Sort), + guard = 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() + ) + } + + 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, 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 -> { + if (type == GoBasicTypes.STRING) mkString("") else ctx.typeToSort(type).sampleUValue() + } + is NamedType -> { + if (type.underlying() is InterfaceType) ctx.nullRef else 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..31bea6cc79 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt @@ -0,0 +1,84 @@ +package org.usvm.state + +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 + +enum class GoFlowStatus { + NORMAL, + PANIC, + DEFER, +} + +class GoStateData( + val flowStack: ArrayDeque = ArrayDeque(), +) { + internal val arrayViews: MutableMap = hashMapOf() + + internal val pointerTargets: MutableMap> = hashMapOf() + + internal val pointerConversions: MutableMap = hashMapOf() + + internal var pendingArrayCopy: GoArrayCopyOperation? = null + internal var builtinResult: UExpr? = null + + private val deferredCalls: ArrayDeque> = ArrayDeque() + + val flowStatus: GoFlowStatus + get() = flowStack.last() + + fun pushDeferredFrame() { + deferredCalls.addLast(ArrayDeque()) + } + + 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) + it.pendingArrayCopy = pendingArrayCopy + it.builtinResult = builtinResult + deferredCalls.forEach { calls -> it.deferredCalls.addLast(ArrayDeque(calls)) } + } + + fun mergeWith(other: GoStateData): GoStateData? { + 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 + ) { + return null + } + return clone() + } + + private fun clonedFlowStack(): ArrayDeque { + val newStack = ArrayDeque() + newStack.addAll(flowStack) + 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..0b8f0cc1da --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoValueCopy.kt @@ -0,0 +1,58 @@ +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, + 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(source.unbox(reference, addressSort), underlying, source), type) + } + + return when (underlying) { + is StructType -> { + val fields = underlying.fields.orEmpty().mapIndexed { index, fieldType -> + copyValue(source.memory.readField(reference, index, typeToSort(fieldType)), fieldType, source) + } + mkTuple(type, fields = fields.toTypedArray()) + } + is ArrayType -> { + 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 = source.memory.readGoArrayIndex( + view.backing, + mkSizeAddExpr(view.offset, mkSizeExpr(index)), + view.storageType, + sort, + ) + copyValue(element, elementType, source).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/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..77ab552478 --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt @@ -0,0 +1,96 @@ +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.SliceType +import org.jacodb.go.api.StructType +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, + private val methodSets: Map>, +) : 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 -> { + // A pointer value is valid even when its pointee is an interface or a recursive pointer type. + true + } + else -> { + false + } + } + + override fun isFinal(type: GoType): Boolean = when (type) { + is InterfaceType -> false + is NamedType -> type.underlying() !is InterfaceType + else -> true + } + + 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) } + } + } + + override fun isSupertype(supertype: GoType, type: GoType): Boolean = when { + // 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 -> { + 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 -> { + methodSets.getValue(impl).containsAll(iface.methods) + } + impl is PointerType -> { + impl.baseType.underlying() !is InterfaceType && methodSets.getValue(impl).containsAll(iface.methods) + } + 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/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/samples/GoExamplesReplayTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt new file mode 100644 index 0000000000..a1060bc1ca --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoExamplesReplayTest.kt @@ -0,0 +1,97 @@ +package org.usvm.samples + +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 kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +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(), message = "Replay needs generated inputs") + val request = buildJsonObject { + put("method", method) + put( + "inputs", + buildJsonArray { executions.forEach { add(toJson(it.arguments)) } } + ) + } + + val nativeResults = replayWithNativeGo( + executable = generatedGoFile("native-examples-replay.test"), + testName = "TestReplayExamples", + request = request, + timeout = 10.seconds, + ).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" } + } + + 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/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/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 new file mode 100644 index 0000000000..5d5c852796 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/GoNativeTestRunner.kt @@ -0,0 +1,51 @@ +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 kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +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 }, 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 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/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..dbe0a27ba6 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/SampleCoverageTest.kt @@ -0,0 +1,86 @@ +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(), 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)) + // 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, + 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.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, + 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.types.ValueRegressionTest::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..e295128dcb --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/arithmetic/ArithmeticRegressionTest.kt @@ -0,0 +1,164 @@ +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 + @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") + + @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) + } +} 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..77072cfa9c --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapRegressionTest.kt @@ -0,0 +1,90 @@ +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 + @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() = 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 = "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) + }, + ) + } +} 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/maps/SlowMapIterationTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt new file mode 100644 index 0000000000..9b479b6622 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/SlowMapIterationTest.kt @@ -0,0 +1,46 @@ +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 + +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 { + 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/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/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/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/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/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/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/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/samples/serialization/ModelTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt new file mode 100644 index 0000000000..2e2f190e98 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt @@ -0,0 +1,23 @@ +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 + +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/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/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() + }, + ) + } +} 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..2783fb713e --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfacesTest.kt @@ -0,0 +1,121 @@ +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() { + // The return after this guaranteed failing assertion is unreachable in Go. + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + + 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/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 new file mode 100644 index 0000000000..2156bfc2a1 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/unsupported/GoUnsupportedTest.kt @@ -0,0 +1,42 @@ +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" + ) + + @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) + } +} diff --git a/usvm-go/src/test/resources/logback.xml b/usvm-go/src/test/resources/logback.xml new file mode 100644 index 0000000000..21170a981d --- /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