diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be76854afa..029246a731 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,9 +60,9 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Compile Go prototype and run lint + - name: Run Go regression tests and lint run: >- - ./gradlew :usvm-go:compileTestKotlin :usvm-go:detektMain :usvm-go:detektTest + ./gradlew :usvm-go:check :usvm-go:detektMain :usvm-go:detektTest --configure-on-demand --no-daemon --max-workers=1 - name: Check Go exporter variadic method signatures diff --git a/usvm-core/src/main/kotlin/org/usvm/Composition.kt b/usvm-core/src/main/kotlin/org/usvm/Composition.kt index f8863982fb..7f23afc945 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Composition.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Composition.kt @@ -122,8 +122,6 @@ open class UComposer( override fun transform(expr: UConcreteHeapRef): UExpr = expr override fun transform(expr: UNullRef): UExpr = memory.nullRef() - - override fun transform(expr: ULValuePointer): UExpr = expr } @Suppress("NOTHING_TO_INLINE") diff --git a/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt b/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt index 3a6b173cb7..57e2e2640c 100644 --- a/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt +++ b/usvm-core/src/main/kotlin/org/usvm/ExprTransformer.kt @@ -67,8 +67,6 @@ interface UTransformer : KTransformer { fun transform(expr: UConcreteHeapRef): UExpr fun transform(expr: UNullRef): UExpr - - fun transform(expr: ULValuePointer): UExpr } abstract class UExprTransformer( diff --git a/usvm-core/src/main/kotlin/org/usvm/Expressions.kt b/usvm-core/src/main/kotlin/org/usvm/Expressions.kt index 3139c3f5c8..927c8168ba 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Expressions.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Expressions.kt @@ -21,7 +21,6 @@ import io.ksmt.sort.KBvSort import io.ksmt.sort.KFpSort import io.ksmt.sort.KSort import io.ksmt.sort.KUninterpretedSort -import org.usvm.memory.ULValue import org.usvm.memory.USymbolicCollection import org.usvm.memory.USymbolicCollectionId import kotlin.contracts.ExperimentalContracts @@ -316,30 +315,6 @@ class UIsSupertypeExpr internal constructor( //endregion -//region Pointer Semantics - -class ULValuePointer( - ctx: UContext<*>, - var lvalue: ULValue<*, *> -) : UExpr(ctx) { - override val sort: UAddressSort = ctx.addressSort - - override fun accept(transformer: KTransformerBase): KExpr { - require(transformer is UTransformer<*, *>) { "Expected a UTransformer, but got: $transformer" } - return transformer.transform(this) - } - - override fun internEquals(other: Any): Boolean = structurallyEqual(other) { lvalue } - - override fun internHashCode(): Int = hash(lvalue) - - override fun print(printer: ExpressionPrinter) { - printer.append("&$lvalue") - } -} - -//endregion - //region Utils val UBoolExpr.isFalse get() = this == ctx.falseExpr diff --git a/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt b/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt index 44163af064..3fbee5d8ab 100644 --- a/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt +++ b/usvm-core/src/main/kotlin/org/usvm/api/collection/PrimitiveMapCollectionApi.kt @@ -27,7 +27,12 @@ import org.usvm.regions.Region import org.usvm.sizeSort object PrimitiveMapCollectionApi { - fun > UState.symbolicPrimitiveMapGet( + fun < + MapType, + KeySort : USort, + ValueSort : USort, + Reg : Region, + > UState.symbolicPrimitiveMapGet( mapRef: UHeapRef, key: UExpr, mapType: MapType, @@ -35,14 +40,22 @@ object PrimitiveMapCollectionApi { keyInfo: USymbolicCollectionKeyInfo, Reg>, ): UExpr = memory.read(UMapEntryLValue(key.sort, valueSort, mapRef, key, mapType, keyInfo)) - fun > UState.symbolicPrimitiveMapContains( + 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 > UState.symbolicPrimitiveMapAnyKey( + fun < + MapType, + KeySort : USort, + Reg : Region, + > UState.symbolicPrimitiveMapAnyKey( mapRef: UHeapRef, mapType: MapType, keySort: KeySort, @@ -66,7 +79,14 @@ object PrimitiveMapCollectionApi { } } - fun , KeySort : USort, ValueSort : USort, Reg : Region> UState.symbolicPrimitiveMapPut( + fun < + MapType, + USizeSort : USort, + Ctx : UContext, + KeySort : USort, + ValueSort : USort, + Reg : Region, + > UState.symbolicPrimitiveMapPut( mapRef: UHeapRef, key: UExpr, value: UExpr, @@ -86,7 +106,13 @@ object PrimitiveMapCollectionApi { memory.write(UMapLengthLValue(mapRef, mapType, sizeSort), updatedSize, keyIsNew) } - fun , KeySort : USort, Reg : Region> UState.symbolicPrimitiveMapRemove( + fun < + MapType, + USizeSort : USort, + Ctx : UContext, + KeySort : USort, + Reg : Region, + > UState.symbolicPrimitiveMapRemove( mapRef: UHeapRef, key: UExpr, mapType: MapType, @@ -97,14 +123,20 @@ object PrimitiveMapCollectionApi { val keyIsInMap = memory.read(mapContainsLValue) - // todo: skip values update? memory.write(mapContainsLValue, rvalue = falseExpr, guard = trueExpr) val updatedSize = mkSizeSubExpr(currentSize, mkSizeExpr(1)) memory.write(UMapLengthLValue(mapRef, mapType, sizeSort), updatedSize, keyIsInMap) } - fun , KeySort : USort, ValueSort : USort, Reg : Region> UState.symbolicPrimitiveMapMergeInto( + fun < + MapType, + USizeSort : USort, + Ctx : UContext, + KeySort : USort, + ValueSort : USort, + Reg : Region, + > UState.symbolicPrimitiveMapCopyIntoEmpty( dstRef: UHeapRef, srcRef: UHeapRef, mapType: MapType, @@ -114,14 +146,21 @@ object PrimitiveMapCollectionApi { ) = 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.mapMerge( + srcRef, + dstRef, + mapType, + keySort, + valueSort, + keyInfo, + containsSetId.uncheckedCast(), + guard = trueExpr + ) memory.setUnion(srcRef, dstRef, mapType, keySort, keyInfo, guard = trueExpr) - // todo: precise map size approximation? - // val sizeLowerBound = mkIte(mkBvSignedGreaterExpr(srcMapSize, dstMapSize), srcMapSize, dstMapSize) - val sizeUpperBound = mkSizeAddExpr(srcMapSize, dstMapSize) - memory.write(UMapLengthLValue(dstRef, mapType, sizeSort), sizeUpperBound, guard = trueExpr) + memory.write(UMapLengthLValue(dstRef, mapType, sizeSort), srcMapSize, guard = trueExpr) } } diff --git a/usvm-core/src/main/kotlin/org/usvm/collection/set/ref/USymbolicRefSetUnionAdapter.kt b/usvm-core/src/main/kotlin/org/usvm/collection/set/ref/USymbolicRefSetUnionAdapter.kt index 6d5e1a354a..50f6b4a6ca 100644 --- a/usvm-core/src/main/kotlin/org/usvm/collection/set/ref/USymbolicRefSetUnionAdapter.kt +++ b/usvm-core/src/main/kotlin/org/usvm/collection/set/ref/USymbolicRefSetUnionAdapter.kt @@ -157,9 +157,8 @@ class UInputToAllocatedSymbolicRefSetUnionAdapter( override fun collectSetElements(elements: USymbolicSetElementsCollector.Elements) { val setElements = USymbolicSetElementsCollector.collect(setOfKeys.updates) - if (setElements.isInput) { - elements.isInput = true - } + // The initial input collection can contain elements absent from its update tree. + elements.isInput = true setElements.elements.mapTo(elements.elements) { it.second } } @@ -211,9 +210,8 @@ class UInputToInputSymbolicRefSetUnionAdapter( override fun collectSetElements(elements: USymbolicSetElementsCollector.Elements>) { val setElements = USymbolicSetElementsCollector.collect(setOfKeys.updates) - if (setElements.isInput) { - elements.isInput = true - } + // The initial input collection can contain elements absent from its update tree. + elements.isInput = true setElements.elements.mapTo(elements.elements) { dstSetRef to it.second } } diff --git a/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt b/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt index 18722e300c..45f4499fa7 100644 --- a/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt +++ b/usvm-core/src/main/kotlin/org/usvm/solver/ExprTranslator.kt @@ -17,7 +17,6 @@ import org.usvm.UIndexedMethodReturnValue import org.usvm.UIsExpr import org.usvm.UIsSubtypeExpr import org.usvm.UIsSupertypeExpr -import org.usvm.ULValuePointer import org.usvm.UNullRef import org.usvm.URegisterReading import org.usvm.USort @@ -95,11 +94,6 @@ open class UExprTranslator( return const } - override fun transform(expr: ULValuePointer): KExpr { - val const = expr.sort.mkConst("&${expr.lvalue}") - return const - } - override fun transform(expr: UConcreteHeapRef): KExpr { require(isStaticHeapRef(expr)) { "Unexpected ref: $expr" } diff --git a/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt b/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt index 865fa7e12a..64c5820f73 100644 --- a/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt +++ b/usvm-core/src/main/kotlin/org/usvm/solver/USoftConstraintsProvider.kt @@ -31,7 +31,6 @@ import org.usvm.UExpr import org.usvm.UIndexedMethodReturnValue import org.usvm.UIsSubtypeExpr import org.usvm.UIsSupertypeExpr -import org.usvm.ULValuePointer import org.usvm.UNullRef import org.usvm.URegisterReading import org.usvm.USort @@ -125,8 +124,6 @@ open class USoftConstraintsProvider( override fun transform(expr: UNullRef): UExpr = expr - override fun transform(expr: ULValuePointer): UExpr = expr - override fun transform(expr: UIsSubtypeExpr): UBoolExpr = expr override fun transform(expr: UIsSupertypeExpr): UBoolExpr = expr diff --git a/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt b/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt index 87eb953b77..b503971d64 100644 --- a/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt +++ b/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt @@ -253,6 +253,35 @@ class ObjectMapTest : SymbolicCollectionTestBase() { testMapMerge(symbolicMap0, symbolicMap1) } + @Test + fun testCopiedInputMapCanSelectAnInitialKey() = testInputCopyRetainsUnknownKey(throughSecondCopy = false) + + @Test + fun testRepeatedInputMapCopyCanSelectAnInitialKey() = testInputCopyRetainsUnknownKey(throughSecondCopy = true) + + private fun testInputCopyRetainsUnknownKey(throughSecondCopy: Boolean) = scope.doWithState { + val source = ctx.mkRegisterReading(idx = 99, sort = ctx.addressSort) + val firstCopy = mkSymbolicObjectMap(mapType) + + symbolicObjectMapMergeInto(firstCopy, source, mapType, ctx.sizeSort) + val copy = if (throughSecondCopy) { + val secondCopy = mkSymbolicObjectMap(mapType) + symbolicObjectMapMergeInto(secondCopy, firstCopy, mapType, ctx.sizeSort) + secondCopy + } else { + firstCopy + } + + val selectedKey = symbolicObjectMapAnyKey(copy, mapType) + val sourceContains = symbolicObjectMapContains(source, selectedKey, mapType) + val copyContains = symbolicObjectMapContains(copy, selectedKey, mapType) + + checkWithSolver { + assertPossible { mkAnd(mkNot(mkHeapRefEq(source, nullRef)), sourceContains, copyContains) } + assertImpossible { mkAnd(copyContains, mkNot(sourceContains)) } + } + } + @Test fun testMapRetrieveAnyKey() = scope.doWithState { callStack.push(method = Any(), returnSite = null) 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/README.md b/usvm-go/README.md new file mode 100644 index 0000000000..9dc27ff744 --- /dev/null +++ b/usvm-go/README.md @@ -0,0 +1,161 @@ +# 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 is not + implemented. String range decodes UTF-8 runes and byte indexes, consuming each malformed + byte as RuneError with width one. 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 index c5d3fd61c7..a8c131c39f 100644 --- a/usvm-go/build.gradle.kts +++ b/usvm-go/build.gradle.kts @@ -1,29 +1,86 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import io.gitlab.arturbosch.detekt.Detekt +import java.time.Duration plugins { kotlin("jvm") - id("org.gradle.application") id("usvm.kotlin-conventions") - id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" -} - -tasks { - withType { - kotlinOptions { - allWarningsAsErrors = false - } - } + 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) - implementation(Libs.slf4j_simple) - testImplementation(Libs.logback) -} \ No newline at end of file +} + +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/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/main.go b/usvm-go/src/main/go/main.go index c6491b6498..29f402d145 100644 --- a/usvm-go/src/main/go/main.go +++ b/usvm-go/src/main/go/main.go @@ -9,6 +9,7 @@ import ( ) 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") @@ -47,7 +48,7 @@ func main() { CheckError(err) if !*dryRun { - ssaInfo.Write() + 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 index c9cd53da4d..cd4008890a 100644 --- a/usvm-go/src/main/go/model.go +++ b/usvm-go/src/main/go/model.go @@ -1,6 +1,8 @@ package main import ( + "go/types" + "golang.org/x/tools/go/ssa" ) @@ -79,8 +81,11 @@ 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 + program *ssa.Program + typeNames map[types.Type]string + typesByName map[string][]types.Type } type Member interface { @@ -105,8 +110,9 @@ type NamedConst struct { } type NamedConstValue struct { - Type string `yaml:"type" json:"type"` - Value string `yaml:"value" json:"value"` + Bytes *string `yaml:"bytes,omitempty" json:"bytes,omitempty"` + Type string `yaml:"type" json:"type"` + Value string `yaml:"value" json:"value"` } type MemberGlobal struct { @@ -469,7 +475,8 @@ type Named struct { type Pointer struct { CommonType `yaml:"-,inline"` - Elem string `yaml:"elem" json:"elem"` + Elem string `yaml:"elem" json:"elem"` + Methods []string `yaml:"methods,omitempty" json:"methods,omitempty"` } type Signature struct { diff --git a/usvm-go/src/main/go/pack.go b/usvm-go/src/main/go/pack.go index bbcaed6c0f..b44042210d 100644 --- a/usvm-go/src/main/go/pack.go +++ b/usvm-go/src/main/go/pack.go @@ -1,10 +1,12 @@ package main import ( + "encoding/base64" "go/constant" "go/types" "log" "sort" + "strconv" "strings" "github.com/samber/lo" @@ -24,8 +26,37 @@ func PackPackage(in *ssa.Package) Package { return p } +// Type.String omits a function-local declaration's scope. Export identities must not. +func (p *Package) TypeName(typ types.Type) string { + if p.typeNames == nil { + p.typeNames = make(map[types.Type]string) + p.typesByName = make(map[string][]types.Type) + } + if name, exists := p.typeNames[typ]; exists { + return name + } + + base := typ.String() + candidates := p.typesByName[base] + for _, candidate := range candidates { + if types.Identical(candidate, typ) { + name := p.typeNames[candidate] + p.typeNames[typ] = name + return name + } + } + + name := base + if len(candidates) > 0 { + name += "#" + strconv.Itoa(len(candidates)+1) + } + p.typeNames[typ] = name + p.typesByName[base] = append(candidates, typ) + return name +} + func (p *Package) AddType(typ types.Type) { - name := typ.String() + name := p.TypeName(typ) if p.Types[name] != nil { return } @@ -39,7 +70,7 @@ func (p *Package) AddType(typ types.Type) { common.Type = AliasType p.Types[name] = Alias{ CommonType: common, - From: types.Unalias(t).String(), + From: p.TypeName(types.Unalias(t)), } p.AddType(types.Unalias(t)) case *types.Array: @@ -47,7 +78,7 @@ func (p *Package) AddType(typ types.Type) { p.Types[name] = Array{ CommonType: common, Len: t.Len(), - Elem: t.Elem().String(), + Elem: p.TypeName(t.Elem()), } p.AddType(t.Elem()) case *types.Basic: @@ -58,7 +89,7 @@ func (p *Package) AddType(typ types.Type) { p.Types[name] = Chan{ CommonType: common, Dir: int(t.Dir()), - Elem: t.Elem().String(), + Elem: p.TypeName(t.Elem()), } p.AddType(t.Elem()) case *types.Interface: @@ -71,8 +102,8 @@ func (p *Package) AddType(typ types.Type) { common.Type = MapType p.Types[name] = Map{ CommonType: common, - Key: t.Key().String(), - Elem: t.Elem().String(), + Key: p.TypeName(t.Key()), + Elem: p.TypeName(t.Elem()), } p.AddType(t.Key()) p.AddType(t.Elem()) @@ -80,7 +111,7 @@ func (p *Package) AddType(typ types.Type) { common.Type = NamedType p.Types[name] = Named{ CommonType: common, - Underlying: t.Underlying().String(), + Underlying: p.TypeName(t.Underlying()), Methods: p.PackMethods(t), } // add members after adding type to avoid stack overflow @@ -92,11 +123,13 @@ func (p *Package) AddType(typ types.Type) { p.Members = append(p.Members, p.PackMember(f)) } p.AddType(t.Underlying()) + p.AddType(types.NewPointer(t)) case *types.Pointer: common.Type = PointerType p.Types[name] = Pointer{ CommonType: common, - Elem: t.Elem().String(), + Elem: p.TypeName(t.Elem()), + Methods: p.PackMethods(t), } p.AddType(t.Elem()) case *types.Signature: @@ -110,14 +143,14 @@ func (p *Package) AddType(typ types.Type) { common.Type = SliceType p.Types[name] = Slice{ CommonType: common, - Elem: t.Elem().String(), + Elem: p.TypeName(t.Elem()), } 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()) + fields = append(fields, p.TypeName(field.Type())) p.AddType(field.Type()) } common.Type = StructType @@ -151,6 +184,9 @@ func (p *Package) PackMembers(membersMap map[string]ssa.Member) { } } + sort.SliceStable(members, func(i, j int) bool { + return members[i].String() < members[j].String() + }) for _, member := range members { p.Members = append(p.Members, p.PackMember(member)) } @@ -167,10 +203,7 @@ func (p *Package) PackMember(in ssa.Member) Member { common.Type = NamedConstMember return NamedConst{ CommonMember: common, - Value: NamedConstValue{ - Type: member.Value.Type().String(), - Value: member.Value.Value.String(), - }, + Value: p.packConstant(member.Value.Type(), member.Value.Value), } case *ssa.Global: p.AddType(member.Type()) @@ -179,7 +212,7 @@ func (p *Package) PackMember(in ssa.Member) Member { return MemberGlobal{ CommonMember: common, Index: int(member.Pos()), - GoType: member.Type().String(), + GoType: p.TypeName(member.Type()), } case *ssa.Type: p.AddType(member.Type()) @@ -229,7 +262,7 @@ func (p *Package) PackParameter(in *ssa.Parameter, index int) Parameter { return Parameter{ CommonValue: CommonValue{ Type: ParameterValue, - GoType: in.Type().String(), + GoType: p.TypeName(in.Type()), Name: in.Name(), }, Index: index, @@ -244,7 +277,7 @@ 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()) + returnTypes = append(returnTypes, p.TypeName(in.At(i).Type())) } return returnTypes @@ -270,7 +303,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = UnOpInstruction return UnOp{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Op: inst.Op.String(), Register: inst.Name(), Argument: p.PackValue(inst.X), @@ -280,7 +313,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = BinOpInstruction return BinOp{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Op: inst.Op.String(), Register: inst.Name(), First: p.PackValue(inst.X), @@ -294,7 +327,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { } return Call{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Value: p.PackValue(inst.Call.Value), Method: method, @@ -304,7 +337,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = ChangeInterfaceInstruction return ChangeInterface{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Value: p.PackValue(inst.X), } @@ -312,7 +345,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = ChangeTypeInstruction return ChangeType{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Value: p.PackValue(inst.X), } @@ -320,7 +353,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = ConvertInstruction return Convert{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Value: p.PackValue(inst.X), } @@ -328,7 +361,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = SliceToArrayPointerInstruction return SliceToArrayPointer{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Value: p.PackValue(inst.X), } @@ -336,7 +369,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = MakeInterfaceInstruction return MakeInterface{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Value: p.PackValue(inst.X), } @@ -344,7 +377,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = ExtractInstruction return Extract{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Tuple: p.PackValue(inst.Tuple), Index: inst.Index, @@ -352,8 +385,8 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { case *ssa.Slice: var ( low ssa.Value = ssa.NewConst(constant.MakeInt64(0), types.Typ[types.Int]) - high ssa.Value = ssa.NewConst(constant.MakeInt64(-1), types.Typ[types.Int]) - mx ssa.Value = ssa.NewConst(constant.MakeInt64(-1), types.Typ[types.Int]) + 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 @@ -367,7 +400,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = SliceInstruction return SliceInst{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Collection: p.PackValue(inst.X), Low: p.PackValue(low), @@ -445,7 +478,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { p.AddType(goType) return Alloc{ CommonInstruction: common, - GoType: goType.String(), + GoType: p.TypeName(goType), Register: inst.Name(), Comment: inst.Comment, } @@ -453,7 +486,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = MakeSliceInstruction return MakeSlice{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Len: p.PackValue(inst.Len), Cap: p.PackValue(inst.Cap), @@ -466,7 +499,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { } return MakeMap{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Reserve: reserve, } @@ -474,7 +507,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = RangeInstruction return Range{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Collection: p.PackValue(inst.X), } @@ -482,7 +515,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = NextInstruction return Next{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Iter: p.PackValue(inst.Iter), IsString: inst.IsString, @@ -491,7 +524,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = FieldAddrInstruction return FieldAddr{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Struct: p.PackValue(inst.X), Field: inst.Field, @@ -500,7 +533,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = FieldInstruction return Field{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Struct: p.PackValue(inst.X), Field: inst.Field, @@ -509,7 +542,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = IndexAddrInstruction return IndexAddr{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Collection: p.PackValue(inst.X), Index: p.PackValue(inst.Index), @@ -518,7 +551,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = IndexInstruction return Index{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Collection: p.PackValue(inst.X), Index: p.PackValue(inst.Index), @@ -527,7 +560,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = LookupInstruction return Lookup{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Map: p.PackValue(inst.X), Key: p.PackValue(inst.Index), @@ -545,10 +578,10 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = TypeAssertInstruction return TypeAssert{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Value: p.PackValue(inst.X), - AssertedType: inst.AssertedType.String(), + AssertedType: p.TypeName(inst.AssertedType), } case *ssa.MakeClosure: common.Type = MakeClosureInstruction @@ -562,7 +595,7 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { common.Type = PhiInstruction return Phi{ CommonInstruction: common, - GoType: inst.Type().String(), + GoType: p.TypeName(inst.Type()), Register: inst.Name(), Edges: lo.Map(inst.Edges, p.PackValueIdx), } @@ -583,23 +616,16 @@ func (p *Package) PackInstruction(in ssa.Instruction, index int) Instruction { func (p *Package) PackValue(in ssa.Value) Value { p.AddType(in.Type()) common := CommonValue{ - GoType: in.Type().String(), + GoType: p.TypeName(in.Type()), Name: in.Name(), } switch value := in.(type) { case *ssa.Const: common.Type = ConstValue - v := "nil" - if value.Value != nil { - v = value.Value.String() - } return Const{ CommonValue: common, - Value: NamedConstValue{ - Type: value.Type().String(), - Value: v, - }, + Value: p.packConstant(value.Type(), value.Value), } case *ssa.Global: common.Type = GlobalValue @@ -640,10 +666,11 @@ func (p *Package) PackValueIdx(in ssa.Value, _ int) Value { return p.PackValue(in) } -func (p *Package) PackMethods(in WithMethods) []string { - methods := make([]string, 0) - for i := 0; i < in.NumMethods(); i++ { - method := in.Method(i) +func (p *Package) PackMethods(in types.Type) []string { + methodSet := types.NewMethodSet(in) + methods := make([]string, 0, methodSet.Len()) + for i := 0; i < methodSet.Len(); i++ { + method := methodSet.At(i).Obj() signature := method.Type().(*types.Signature) methods = append(methods, method.Name()+p.PackMethodParams(signature)+p.PackMethodResults(signature.Results())) } @@ -656,7 +683,7 @@ func (p *Package) PackMethodParams(signature *types.Signature) string { if signature.Variadic() { last := len(params) - 1 slice := signature.Params().At(last).Type().(*types.Slice) - params[last] = "..." + slice.Elem().String() + params[last] = "..." + p.TypeName(slice.Elem()) } return "(" + strings.Join(params, ", ") + ")" @@ -677,7 +704,7 @@ func (p *Package) PackMethodResults(in *types.Tuple) string { 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()) + out = append(out, p.TypeName(in.At(i).Type())) } return out } @@ -716,3 +743,33 @@ type WithMethods interface { NumMethods() int Method(i int) *types.Func } + +func (p *Package) packConstant(typ types.Type, value constant.Value) NamedConstValue { + result := packConstant(typ, value) + result.Type = p.TypeName(typ) + return result +} + +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/pack_test.go b/usvm-go/src/main/go/pack_test.go index 7342f3696a..d7d28f589d 100644 --- a/usvm-go/src/main/go/pack_test.go +++ b/usvm-go/src/main/go/pack_test.go @@ -53,3 +53,36 @@ func TestMethodSignaturesPreserveVariadicParameters(t *testing.T) { }) } } + +func TestTypeIdentitiesPreserveDistinctLocalDeclarations(t *testing.T) { + pkg := types.NewPackage("records", "records") + first := types.NewNamed( + types.NewTypeName(token.Pos(1), pkg, "record", nil), + types.NewStruct([]*types.Var{types.NewVar(token.NoPos, pkg, "number", types.Typ[types.Int])}, nil), + nil, + ) + second := types.NewNamed( + types.NewTypeName(token.Pos(2), pkg, "record", nil), + types.NewStruct([]*types.Var{types.NewVar(token.NoPos, pkg, "pointer", types.NewPointer(types.Typ[types.Int]))}, nil), + nil, + ) + exporter := Package{Types: make(map[string]Type)} + + exporter.AddType(first) + exporter.AddType(second) + firstName, secondName := exporter.TypeName(first), exporter.TypeName(second) + if firstName == secondName { + t.Fatal("distinct local named types share an exported identity") + } + firstRecord := exporter.Types[exporter.Types[firstName].(Named).Underlying].(Struct) + secondRecord := exporter.Types[exporter.Types[secondName].(Named).Underlying].(Struct) + if firstRecord.Fields[0] != "int" || secondRecord.Fields[0] != "*int" { + t.Fatalf("local record fields changed: first=%v, second=%v", firstRecord.Fields, secondRecord.Fields) + } + if exporter.TypeName(types.NewPointer(first)) == exporter.TypeName(types.NewPointer(second)) { + t.Fatal("pointers to distinct local types share an exported identity") + } + if exporter.TypeName(types.NewPointer(first)) != exporter.TypeName(types.NewPointer(first)) { + t.Fatal("identical pointer types have different exported identities") + } +} diff --git a/usvm-go/src/main/go/regressions/equality_range.go b/usvm-go/src/main/go/regressions/equality_range.go new file mode 100644 index 0000000000..2047641791 --- /dev/null +++ b/usvm-go/src/main/go/regressions/equality_range.go @@ -0,0 +1,162 @@ +package regressions + +func mapDeletedDuringRange() int { + values := map[int]int{1: 1, 2: 1} + count := 0 + for range values { + delete(values, 1) + delete(values, 2) + count++ + } + return count +} + +func mapRangeCurrentValues() int { + values := map[int]int{1: 1, 2: 1} + sum := 0 + for _, value := range values { + sum += value + values[1] = 9 + values[2] = 9 + } + return sum +} + +func mapDeletedReferenceKeysDuringRange() int { + first, second := 1, 2 + values := map[*int]int{&first: 1, &second: 1} + count := 0 + for range values { + delete(values, &first) + delete(values, &second) + count++ + } + return count +} + +func utf8RangeRunesAndIndexes() int { + result := 0 + for index, value := range "AĆ©ā‚¬šŸ˜€" { + result += int(value) + index*1000000 + } + return result +} + +func utf8RangeInvalidBytes() int { + result := 0 + // Invalid start, overlong encodings, surrogate, out-of-range rune, and truncated tail. + for index, value := range "\xff\xc0\xaf\xe0\x80\x80\xed\xa0\x80\xf4\x90\x80\x80\xf0\x9f\x98" { + result += int(value) + index*1000000 + } + return result +} + +func utf8RangeBoundaryRunes() int { + result := 0 + for index, value := range "\x00\x7f\xc2\x80\xdf\xbf\xe0\xa0\x80\xed\x9f\xbf\xee\x80\x80\xef\xbf\xbf\xf0\x90\x80\x80\xf4\x8f\xbf\xbf" { + result += int(value) + index*1000000 + } + return result +} + +func utf8RangeMalformedContinuation() int { + result := 0 + for index, value := range "\xe2A\x82\xf0\x9fB\x80" { + result += int(value) + index*1000000 + } + return result +} + +func utf8RangeNamedString() int { + type text string + result := 0 + for index, value := range text("Ć©šŸ˜€") { + result += int(value) + index*1000000 + } + return result +} + +func copiedArrayEquality() bool { + original := [2]int{7, 9} + copied := original + return original == copied +} + +func copiedArrayInequality() bool { + original := [2]int{7, 9} + copied := original + copied[1] = 8 + return original != copied +} + +func copiedStructEquality() bool { + type record struct { + numbers [2]int + text string + } + original := record{numbers: [2]int{7, 9}, text: "same"} + copied := original + return original == copied +} + +func structPointerFieldsCompareIdentity() bool { + type record struct{ pointer *int } + first, second := 7, 7 + left := record{pointer: &first} + right := record{pointer: &second} + return left == right +} + +func copiedStructSharesPointerIdentity() bool { + type record struct{ pointer *int } + value := 7 + original := record{pointer: &value} + copied := original + return original == copied +} + +func interfaceArrayValueEquality() bool { + array := [1]int{7} + var left any = array + var right any = array + return left == right +} + +func interfaceNamedTypeInequality() bool { + type first int + type second int + var left any = first(7) + var right any = second(7) + return left != right +} + +func interfaceUncomparablePanics() bool { + value := []int{7} + var left any = value + var right any = value + return left == right +} + +func interfaceDifferentUncomparableTypes() bool { + var left any = []int{7} + var right any = map[int]int{7: 7} + return left == right +} + +func structEqualityStopsBeforeUncomparableField() bool { + type record struct { + number int + value any + } + left := record{number: 1, value: []int{7}} + right := record{number: 2, value: []int{7}} + return left == right +} + +func utf8RangeReplacementRune() int { + result := 0 + for index, value := range "\xef\xbf\xbd\xffA" { + result += int(value) + index*1000000 + } + return result +} diff --git a/usvm-go/src/main/go/regressions/map_key_replay_test.go b/usvm-go/src/main/go/regressions/map_key_replay_test.go new file mode 100644 index 0000000000..1f5d2cf78f --- /dev/null +++ b/usvm-go/src/main/go/regressions/map_key_replay_test.go @@ -0,0 +1,124 @@ +package regressions + +import ( + "encoding/json" + "math" + "os" + "strconv" + "testing" +) + +type mapKeyReplayEntry struct { + FloatKeyBits string `json:"floatKeyBits"` + StringKey string `json:"stringKey"` + Value int64 `json:"value"` +} + +type mapKeyReplayInput struct { + LookupKey string `json:"lookupKey"` + LookupFloatBits string `json:"lookupFloatBits"` + Nil bool `json:"nil"` + Entries []mapKeyReplayEntry `json:"entries"` +} + +type mapKeyReplayRequest struct { + Method string `json:"method"` + Inputs []mapKeyReplayInput `json:"inputs"` +} + +func TestReplayMapKeys(t *testing.T) { + filename := os.Getenv("USVM_GO_REPLAY_FILE") + if filename == "" { + t.Skip("No map witnesses supplied") + } + data, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + var request mapKeyReplayRequest + if err := json.Unmarshal(data, &request); err != nil { + t.Fatal(err) + } + results := make([]int, len(request.Inputs)) + for index, input := range request.Inputs { + var floats map[float64]int + var strings map[string]int + if !input.Nil { + floats = make(map[float64]int) + strings = make(map[string]int) + for _, entry := range input.Entries { + strings[entry.StringKey] = int(entry.Value) + if entry.FloatKeyBits != "" { + bits, err := strconv.ParseUint(entry.FloatKeyBits, 16, 64) + if err != nil { + t.Fatal(err) + } + floats[math.Float64frombits(bits)] = int(entry.Value) + } + } + } + var lookupFloat float64 + if input.LookupFloatBits != "" { + bits, err := strconv.ParseUint(input.LookupFloatBits, 16, 64) + if err != nil { + t.Fatal(err) + } + lookupFloat = math.Float64frombits(bits) + } + switch request.Method { + case "inputFloatMapCanonicalGhosts": + results[index] = inputFloatMapCanonicalGhosts(floats, lookupFloat) + case "inputStringMapSymbolicKeyAndExtra": + results[index] = inputStringMapSymbolicKeyAndExtra(strings, input.LookupKey) + case "inputFloatMapZeroRangeSign": + results[index] = inputFloatMapZeroRangeSign(floats) + case "inputFloatMapLookup": + results[index] = inputFloatMapLookup(floats) + case "inputStringMapLookup": + results[index] = inputStringMapLookup(strings) + case "inputFloatMapExtraEntry": + results[index] = inputFloatMapExtraEntry(floats) + case "inputStringMapExtraEntry": + results[index] = inputStringMapExtraEntry(strings) + case "floatMapNaNResult": + values := floatMapNaNResult() + result := len(values) * 100 + for key, value := range values { + if !math.IsNaN(key) { + t.Fatal("Expected NaN key") + } + result += value + } + results[index] = result + case "pointerMapKeyResult": + values := pointerMapKeyResult() + result := len(values) * 100 + for key, value := range values { + if key == nil || *key != 0 { + t.Fatal("Expected distinct pointers to zero") + } + result += value + } + results[index] = result + case "namedStringMapResult": + values := namedStringMapResult() + results[index] = len(values)*100 + values[mapStringKey("same")] + case "namedArrayMapResult": + values := namedArrayMapResult() + result := len(values) * 100 + for key, value := range values { + result += key[0]*10 + key[1] + value + } + results[index] = result + default: + t.Fatal("Unknown map witness method") + } + } + data, err = json.Marshal(results) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filename, data, 0o600); err != nil { + t.Fatal(err) + } +} diff --git a/usvm-go/src/main/go/regressions/map_keys_signatures.go b/usvm-go/src/main/go/regressions/map_keys_signatures.go new file mode 100644 index 0000000000..879f10944a --- /dev/null +++ b/usvm-go/src/main/go/regressions/map_keys_signatures.go @@ -0,0 +1,74 @@ +package regressions + +type mapIntegerKey int +type mapUnsignedKey uint64 +type mapBooleanKey bool + +func namedScalarMapLookup() int { + values := map[mapIntegerKey]int{} + values[mapIntegerKey(1)] = 7 + return values[mapIntegerKey(1)] +} + +func namedScalarMapOverwrite() int { + values := map[mapIntegerKey]int{} + values[mapIntegerKey(1)] = 7 + values[mapIntegerKey(1)] = 9 + return len(values)*100 + values[mapIntegerKey(1)] +} + +func namedScalarMapDelete() int { + values := map[mapIntegerKey]int{mapIntegerKey(1): 7} + delete(values, mapIntegerKey(1)) + return len(values) + values[mapIntegerKey(1)] +} + +func namedScalarMapRange() int { + values := map[mapIntegerKey]int{mapIntegerKey(1): 7, mapIntegerKey(2): 9} + sum := 0 + for key, value := range values { + sum += int(key) + value + } + return sum +} + +func namedBooleanMapKeys() int { + values := map[mapBooleanKey]int{mapBooleanKey(true): 7, mapBooleanKey(false): 9} + values[mapBooleanKey(true)] = 11 + delete(values, mapBooleanKey(false)) + return len(values)*100 + values[mapBooleanKey(true)] +} + +func namedScalarMapResult() map[mapIntegerKey]int { + return map[mapIntegerKey]int{mapIntegerKey(1): 7} +} + +func namedUnsignedMapResult() map[mapUnsignedKey]int { + return map[mapUnsignedKey]int{mapUnsignedKey(18446744073709551615): 7} +} + +type variadicCounter interface{ Count(...int) int } +type sliceCounter interface{ Count([]int) int } +type sliceSignatureReceiver struct{} +type variadicSignatureReceiver struct{} + +func (sliceSignatureReceiver) Count(values []int) int { return len(values) } +func (variadicSignatureReceiver) Count(values ...int) int { return len(values) } + +func sliceMethodDoesNotImplementVariadic() bool { + var value any = sliceSignatureReceiver{} + _, ok := value.(variadicCounter) + return ok +} + +func variadicMethodImplementsVariadic() bool { + var value any = variadicSignatureReceiver{} + _, ok := value.(variadicCounter) + return ok +} + +func variadicMethodDoesNotImplementSlice() bool { + var value any = variadicSignatureReceiver{} + _, ok := value.(sliceCounter) + return ok +} diff --git a/usvm-go/src/main/go/regressions/map_value_keys.go b/usvm-go/src/main/go/regressions/map_value_keys.go new file mode 100644 index 0000000000..fd2500711d --- /dev/null +++ b/usvm-go/src/main/go/regressions/map_value_keys.go @@ -0,0 +1,302 @@ +package regressions + +type mapFloatKey float64 +type mapStringKey string +type mapRecordKey struct { + name string + index int +} + +func negativeMapZero(value float64) float64 { return -value } +func mapNaN(value float64) float64 { return value / value } + +func floatMapZeroLookup() int { + values := map[float64]int{0: 7} + return values[negativeMapZero(0)] +} + +func floatMapZeroOverwrite() int { + values := map[float64]int{0: 7} + values[negativeMapZero(0)] = 9 + return len(values)*10 + values[0] +} + +func floatMapZeroDelete() int { + values := map[float64]int{0: 7} + delete(values, negativeMapZero(0)) + return len(values) +} + +func namedFloatMapZeroLookup() int { + values := map[mapFloatKey]int{0: 7} + return values[mapFloatKey(negativeMapZero(0))] +} + +func floatMapNaNLookup() int { + key := mapNaN(0) + values := map[float64]int{} + values[key] = 7 + values[key] = 9 + value, found := values[key] + // NaN is never found; encode the observation without an unreachable branch. + foundPenalty := map[bool]int{false: 0, true: -1}[found] + delete(values, key) + return len(values)*10 + value + foundPenalty +} + +func floatMapNaNRange() int { + key := mapNaN(0) + values := map[float64]int{} + values[key] = 7 + values[key] = 9 + result := 0 + for key, value := range values { + // Every key is NaN, so reflexivity is observed without an impossible alternative. + reflexivePenalty := map[bool]int{false: 0, true: -1}[key == key] + result += value + reflexivePenalty + } + return result +} + +func stringMapValueLookup() int { + values := map[string]int{"same": 7} + return values["same"] +} + +func namedStringMapValueLookup() int { + values := map[mapStringKey]int{"same": 9} + return values[mapStringKey("same")] +} + +func arrayMapKeySnapshot() int { + key := [2]int{1, 2} + values := map[[2]int]int{key: 7} + key[0] = 9 + return values[[2]int{1, 2}]*10 + values[key] +} + +func structMapValueLookup() int { + values := map[mapRecordKey]int{{name: "same", index: 2}: 11} + return values[mapRecordKey{name: "same", index: 2}] +} + +func arrayMapRangeKeyCopy() int { + values := map[[2]int]int{{1, 2}: 7} + for key := range values { + key[0] = 9 + } + return values[[2]int{1, 2}] +} + +func interfaceMapValueLookup() int { + values := map[any]int{[2]int{1, 2}: 7} + return values[[2]int{1, 2}] +} + +type mapArrayKey [2]int + +func float32MapZeroAndNaN() int { + zero := float32(negativeMapZero(0)) + nan := float32(mapNaN(0)) + values := map[float32]int{0: 7} + values[zero] = 9 + // Signed zeros overwrite one entry; keep both observations in the native checksum. + zeroChecksum := (len(values)-1)*1000 + values[0] - 9 + values[nan] = 7 + values[nan] = 9 + delete(values, zero) + result := len(values)*100 + zeroChecksum + for key, value := range values { + // Every key is NaN, so reflexivity is observed without an impossible alternative. + reflexivePenalty := map[bool]int{false: 0, true: -1}[key == key] + result += value + reflexivePenalty + } + return result +} + +func interfaceMapUncomparableLookup() int { + values := map[any]int{} + key := []int{1} + return values[key] +} + +func interfaceMapUncomparableUpdate() int { + values := map[any]int{} + key := []int{1} + values[key] = 7 + return len(values) +} + +func floatMapNaNResult() map[float64]int { + key := mapNaN(0) + values := map[float64]int{} + values[key] = 7 + values[key] = 9 + return values +} + +func nilPointerMapKeyLookupAndRange() int { + values := map[*int]int{nil: 7} + value, found := values[nil] + foundPenalty := map[bool]int{false: -1000, true: 0}[found] + + rangeChecksum := 0 + for key, rangedValue := range values { + nonNilPenalty := map[bool]int{false: -1000, true: 0}[key == nil] + rangeChecksum += rangedValue + nonNilPenalty + } + return len(values)*100 + value*10 + rangeChecksum + foundPenalty +} + +func pointerMapKeyResult() map[*int]int { + first, second := new(int), new(int) + values := map[*int]int{first: 7, second: 9} + return values +} + +func namedStringMapResult() map[mapStringKey]int { + return map[mapStringKey]int{"same": 9} +} + +func namedArrayMapResult() map[mapArrayKey]int { + return map[mapArrayKey]int{{1, 2}: 7} +} + +func inputFloatMapLookup(values map[float64]int) int { + value, found := values[negativeMapZero(0)] + if found { + return value + } + return -1 +} + +func inputStringMapLookup(values map[string]int) int { + value, found := values["same"] + if found { + return value + } + return -1 +} + +func inputFloatMapExtraEntry(values map[float64]int) int { + if len(values) != 2 { + return -1 + } + value, found := values[0] + if !found { + return -2 + } + return value +} + +func inputStringMapExtraEntry(values map[string]int) int { + if len(values) != 2 { + return -1 + } + value, found := values["same"] + if !found { + return -2 + } + return value +} + +type mapPointerNumber int + +func pointerMapConvertedAlias() int { + pointer := new(int) + values := map[*int]int{pointer: 7} + converted := (*mapPointerNumber)(pointer) + restored := (*int)(converted) + return len(values)*10 + values[restored] +} + +func floatMapZeroRangeAfterOverwrite() int { + values := map[float64]int{0: 7} + values[negativeMapZero(0)] = 9 + result := 0 + for key := range values { + result += map[bool]int{false: 1, true: -1}[1/key < 0] + } + return result +} + +func inputFloatMapZeroRangeSign(values map[float64]int) int { + if len(values) != 1 { + return 0 + } + _, found := values[0] + if !found { + return 0 + } + // A one-entry map containing zero can only range over one of its two signs. + result := 0 + for key := range values { + if 1/key < 0 { + result = -1 + } else { + result = 1 + } + } + return result +} + +type mapMixedKey struct { + number float64 + payload any +} + +func mapStructNaNUncomparablePanics() int { + values := map[mapMixedKey]int{} + key := mapMixedKey{number: mapNaN(0), payload: []int{1}} + values[key] = 7 + return len(values) +} + +func inputStringMapSymbolicKeyAndExtra(values map[string]int, key string) int { + if len(values) != 2 { + return -1 + } + value, found := values[key] + if !found { + return -2 + } + return value +} + +func inputFloatMapCanonicalGhosts(values map[float64]int, key float64) int { + if key != 0 { + return -1 + } + if len(values) != 2 { + return -2 + } + first, firstFound := values[key] + second, secondFound := values[negativeMapZero(key)] + // +/-zero must select the same entry; retain consistency in every replayed result. + inconsistencyCount := map[bool]int{false: 0, true: 1}[firstFound != secondFound] + + map[bool]int{false: 0, true: 1}[first != second] + penalty := map[bool]int{false: 0, true: -3}[inconsistencyCount != 0] + if !firstFound { + return -4 + penalty + } + return first + penalty +} + +func markMapKeyValue(sideEffect *int) int { + *sideEffect = 7 + return 1 +} + +func recoverMapKeyAssignment(sideEffect *int, result *int) { + if recover() != nil { + *result = *sideEffect + } +} + +func mapKeyHashAfterValueEvaluation() (result int) { + sideEffect := 0 + defer recoverMapKeyAssignment(&sideEffect, &result) + values := map[any]int{} + values[[]int{1}] = markMapKeyValue(&sideEffect) + return 0 +} 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..5540a5c057 --- /dev/null +++ b/usvm-go/src/main/go/regressions/semantics_test.go @@ -0,0 +1,241 @@ +package regressions + +import ( + "encoding/json" + "fmt" + "os" + "testing" +) + +func TestNativeOracle(t *testing.T) { + cases := map[string]func() any{ + "mapKeyHashAfterValueEvaluation": func() any { return mapKeyHashAfterValueEvaluation() }, + "mapStructNaNUncomparablePanics": func() any { return mapStructNaNUncomparablePanics() }, + "floatMapZeroRangeAfterOverwrite": func() any { return floatMapZeroRangeAfterOverwrite() }, + "pointerMapConvertedAlias": func() any { return pointerMapConvertedAlias() }, + "float32MapZeroAndNaN": func() any { return float32MapZeroAndNaN() }, + "interfaceMapUncomparableLookup": func() any { return interfaceMapUncomparableLookup() }, + "interfaceMapUncomparableUpdate": func() any { return interfaceMapUncomparableUpdate() }, + "floatMapNaNResult": func() any { return floatMapNaNResult() }, + "nilPointerMapKeyLookupAndRange": func() any { return nilPointerMapKeyLookupAndRange() }, + "pointerMapKeyResult": func() any { return pointerMapKeyResult() }, + "namedStringMapResult": func() any { return namedStringMapResult() }, + "namedArrayMapResult": func() any { return namedArrayMapResult() }, + "floatMapZeroLookup": func() any { return floatMapZeroLookup() }, + "floatMapZeroOverwrite": func() any { return floatMapZeroOverwrite() }, + "floatMapZeroDelete": func() any { return floatMapZeroDelete() }, + "namedFloatMapZeroLookup": func() any { return namedFloatMapZeroLookup() }, + "floatMapNaNLookup": func() any { return floatMapNaNLookup() }, + "floatMapNaNRange": func() any { return floatMapNaNRange() }, + "stringMapValueLookup": func() any { return stringMapValueLookup() }, + "namedStringMapValueLookup": func() any { return namedStringMapValueLookup() }, + "arrayMapKeySnapshot": func() any { return arrayMapKeySnapshot() }, + "structMapValueLookup": func() any { return structMapValueLookup() }, + "arrayMapRangeKeyCopy": func() any { return arrayMapRangeKeyCopy() }, + "interfaceMapValueLookup": func() any { return interfaceMapValueLookup() }, + "namedScalarMapLookup": func() any { return namedScalarMapLookup() }, + "namedScalarMapOverwrite": func() any { return namedScalarMapOverwrite() }, + "namedScalarMapDelete": func() any { return namedScalarMapDelete() }, + "namedScalarMapRange": func() any { return namedScalarMapRange() }, + "namedBooleanMapKeys": func() any { return namedBooleanMapKeys() }, + "namedScalarMapResult": func() any { return namedScalarMapResult() }, + "namedUnsignedMapResult": func() any { return namedUnsignedMapResult() }, + "sliceMethodDoesNotImplementVariadic": func() any { return sliceMethodDoesNotImplementVariadic() }, + "variadicMethodImplementsVariadic": func() any { return variadicMethodImplementsVariadic() }, + "variadicMethodDoesNotImplementSlice": func() any { return variadicMethodDoesNotImplementSlice() }, + "utf8RangeReplacementRune": func() any { return utf8RangeReplacementRune() }, + "mapDeletedDuringRange": func() any { return mapDeletedDuringRange() }, + "mapRangeCurrentValues": func() any { return mapRangeCurrentValues() }, + "mapDeletedReferenceKeysDuringRange": func() any { return mapDeletedReferenceKeysDuringRange() }, + "utf8RangeRunesAndIndexes": func() any { return utf8RangeRunesAndIndexes() }, + "utf8RangeInvalidBytes": func() any { return utf8RangeInvalidBytes() }, + "utf8RangeBoundaryRunes": func() any { return utf8RangeBoundaryRunes() }, + "utf8RangeMalformedContinuation": func() any { return utf8RangeMalformedContinuation() }, + "utf8RangeNamedString": func() any { return utf8RangeNamedString() }, + "copiedArrayEquality": func() any { return copiedArrayEquality() }, + "copiedArrayInequality": func() any { return copiedArrayInequality() }, + "copiedStructEquality": func() any { return copiedStructEquality() }, + "structPointerFieldsCompareIdentity": func() any { return structPointerFieldsCompareIdentity() }, + "copiedStructSharesPointerIdentity": func() any { return copiedStructSharesPointerIdentity() }, + "interfaceArrayValueEquality": func() any { return interfaceArrayValueEquality() }, + "interfaceNamedTypeInequality": func() any { return interfaceNamedTypeInequality() }, + "interfaceUncomparablePanics": func() any { return interfaceUncomparablePanics() }, + "interfaceDifferentUncomparableTypes": func() any { return interfaceDifferentUncomparableTypes() }, + "structEqualityStopsBeforeUncomparableField": func() any { return structEqualityStopsBeforeUncomparableField() }, + "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 index ec69a5d3a2..0eff930aef 100644 --- a/usvm-go/src/main/go/ssa.go +++ b/usvm-go/src/main/go/ssa.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "go/token" + "go/types" "io" "log" "os" @@ -30,6 +31,7 @@ type SSA struct { program *ssa.Program packages []*ssa.Package + intSize int } func NewSSA(packageName string, cfg Config) (*SSA, error) { @@ -69,6 +71,7 @@ func NewSSA(packageName string, cfg Config) (*SSA, error) { output: os.Stdout, program: program, packages: program.AllPackages(), + intSize: int(initialPackages[0].TypesSizes.Sizeof(types.Typ[types.Int]) * 8), } if cfg.DumpSSA { @@ -86,22 +89,23 @@ func NewSSA(packageName string, cfg Config) (*SSA, error) { return s, nil } -func (s *SSA) Write() { +func (s *SSA) Write(outputDir string) { for _, pkg := range s.packages { now := time.Now() - s.writePackage(pkg) + 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) { +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(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 { diff --git a/usvm-go/src/main/go/util.go b/usvm-go/src/main/go/util.go index eaa7906145..c8a04233e7 100644 --- a/usvm-go/src/main/go/util.go +++ b/usvm-go/src/main/go/util.go @@ -3,16 +3,6 @@ package main import ( "io" "log" - "path" - "path/filepath" - "runtime" -) - -var ( - _, filename, _, _ = runtime.Caller(0) - - Root = filepath.Dir(filename) - OutputDir = path.Join(Root, filepath.Dir("../../../out/")) ) func CheckClose(c io.Closer) { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt b/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt index 4c978cb972..1f5fb1d0a8 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoApplicationGraph.kt @@ -24,15 +24,11 @@ class GoApplicationGraph : ApplicationGraph { return emptySequence() } val callExpr = node.callExpr - return sequenceOf(callExpr.callee!!) + return listOfNotNull(callExpr.callee).asSequence() } override fun callers(method: GoMethod): Sequence { - var res = listOf() - for (block in method.blocks) { - res = listOf(res, block.instructions).flatten() - } - return res.asSequence() + return method.blocks.asSequence().flatMap { it.instructions } } override fun entryPoints(method: GoMethod): Sequence { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt b/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt index edde50f1a2..3c80f3df0d 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoComponents.kt @@ -11,7 +11,7 @@ import org.usvm.types.UTypeSystem class GoComponents( private val typeSystem: GoTypeSystem, private val options: UMachineOptions, -): UComponents { +) : UComponents { override val useSolverForForks: Boolean get() = options.useSolverForForks override fun mkTypeSystem(ctx: UContext): UTypeSystem { @@ -21,8 +21,12 @@ class GoComponents( override fun > mkSolver(ctx: Context): USolverBase { val (translator, decoder) = buildTranslatorAndLazyDecoder(ctx) val solver = when (options.solverType) { - SolverType.YICES -> KYicesSolver(ctx) - SolverType.Z3 -> KZ3Solver(ctx) + SolverType.YICES -> { + KYicesSolver(ctx) + } + SolverType.Z3 -> { + KZ3Solver(ctx) + } } val typeSolver = UTypeSolver(typeSystem) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoContext.kt b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt index c73f995fb7..9f3c005764 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoContext.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoContext.kt @@ -13,9 +13,12 @@ import org.usvm.operator.GoUnaryOperator import org.usvm.type.GoBasicTypes import org.usvm.type.GoVoidSort import org.usvm.type.GoVoidValue +import org.usvm.type.mapKeyStorageType +import org.usvm.type.needsMapKeyRepresentative class GoContext( components: UComponents, + val intSize: Int, ) : UContext(components) { val voidSort by lazy { GoVoidSort(this) } @@ -26,69 +29,110 @@ class GoContext( private val methodInfo: MutableMap = hashMapOf() private val globals: MutableMap> = hashMapOf() - fun getMethodInfo(method: GoMethod) = methodInfo[method]!! - - fun setMethodInfo(method: GoMethod) { - val localsCount = method.blocks.flatMap { it.instructions }.filterIsInstance().size - val argumentsCount = method.parameters.size - - setMethodInfo(method, GoMethodInfo(localsCount, argumentsCount, emptyArray())) - } - - fun setMethodInfo(method: GoMethod, parameters: Array>) { + fun getMethodInfo(method: GoMethod): GoMethodInfo = methodInfo.getOrPut(method) { val localsCount = method.blocks.flatMap { it.instructions }.filterIsInstance().size - val freeVariablesCount = getFreeVariablesCount(method) - setMethodInfo(method, GoMethodInfo(localsCount + freeVariablesCount, parameters.size, parameters)) + GoMethodInfo( + variablesCount = localsCount + getFreeVariablesCount(method), + argumentsCount = method.parameters.size, + ) } fun addGlobal(global: GoGlobal, expr: UExpr) { globals[global] = expr } - fun getGlobal(global: GoGlobal): UExpr = globals[global]!! + fun getGlobal(global: GoGlobal): UExpr = globals.getValue(global) fun freeVariableOffset(method: GoMethod) = getArgsCount(method) fun localVariableOffset(method: GoMethod) = getArgsCount(method) + getFreeVariablesCount(method) + internal fun mapKeySort(type: GoType): USort = + if (type.needsMapKeyRepresentative()) addressSort else typeToSort(type.mapKeyStorageType()) + fun typeToSort(type: GoType): USort = when (type) { - is BasicType -> basicTypeToSort(type) - is NamedType -> addressSort - is PointerType -> addressSort - is NullType -> voidSort - else -> addressSort + is BasicType -> { + basicTypeToSort(type) + } + is NamedType -> { + addressSort + } + is PointerType -> { + addressSort + } + is NullType -> { + voidSort + } + else -> { + addressSort + } } fun mkPrimitiveCast(expr: UExpr, to: USort): UExpr = when (to) { - boolSort -> GoUnaryOperator.CastToBool(expr) - bv8Sort -> GoUnaryOperator.CastToInt8(expr) - bv16Sort -> GoUnaryOperator.CastToInt16(expr) - bv32Sort -> GoUnaryOperator.CastToInt32(expr) - bv64Sort -> GoUnaryOperator.CastToInt64(expr) - fp32Sort -> GoUnaryOperator.CastToFloat32(expr) - fp64Sort -> GoUnaryOperator.CastToFloat64(expr) - else -> throw IllegalStateException() - } - - private fun setMethodInfo(method: GoMethod, info: GoMethodInfo) { - methodInfo[method] = info + 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 = methodInfo[method]!!.argumentsCount + private fun getArgsCount(method: GoMethod): Int = getMethodInfo(method).argumentsCount private fun getFreeVariablesCount(method: GoMethod): Int = when (method) { - is GoFunction -> method.freeVars.size - else -> 0 + is GoFunction -> { + method.freeVars.size + } + else -> { + 0 + } } private fun basicTypeToSort(type: BasicType): USort = when (type) { - GoBasicTypes.BOOL -> boolSort - GoBasicTypes.INT, GoBasicTypes.UINT, GoBasicTypes.INT32, GoBasicTypes.UINT32, GoBasicTypes.RUNE -> bv32Sort - GoBasicTypes.INT8, GoBasicTypes.UINT8 -> bv8Sort - GoBasicTypes.INT16, GoBasicTypes.UINT16 -> bv16Sort - GoBasicTypes.INT64, GoBasicTypes.UINT64, GoBasicTypes.UINTPTR, GoBasicTypes.UNSAFE_POINTER -> bv64Sort - GoBasicTypes.FLOAT32 -> fp32Sort - GoBasicTypes.FLOAT64 -> fp64Sort - else -> addressSort + 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 index 91858151ab..f40248b41e 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoDomain.kt @@ -13,16 +13,17 @@ import org.usvm.util.isOsInit class GoMethodInfo( val variablesCount: Int, val argumentsCount: Int, - val arguments: Array>, ) { override fun toString(): String { return "variables: $variablesCount, arguments: $argumentsCount" } } -class GoCall( +data class GoCall( val method: GoMethod, val entrypoint: GoInst, + val arguments: List>? = null, + val freeVariables: List> = emptyList(), ) class GoPackage( @@ -30,6 +31,8 @@ class GoPackage( val methods: List, val globals: List, val types: Map, + val intSize: Int, + val methodSets: Map> = emptyMap(), ) { private val methodsIndex: Map = methods.associateBy { it.metName } @@ -39,8 +42,13 @@ class GoPackage( } 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 } @@ -50,9 +58,9 @@ class GoProgram(packages: List) { fun findMethod(location: GoInstLocation, name: String): GoMethod { if (name.contains('.') && !name.contains('(')) { - val split = name.split('.') - val packageName = split[0] - val methodName = split[1] + val separator = name.lastIndexOf('.') + val packageName = name.substring(0, separator) + val methodName = name.substring(separator + 1) return findPackage(packageName).findMethod(methodName) } @@ -60,7 +68,7 @@ class GoProgram(packages: List) { } fun findInitMethods(packageName: String): List { - return packagesIndex[packageName]!!.methods.filter { it.isInit() }.sortedBy { it.metName } + return packagesIndex.getValue(packageName).methods.filter { it.isInit() }.sortedBy { it.metName } } fun findOsInitMethods(): List { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt index f588036cdc..fd1353dccb 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoExprVisitor.kt @@ -2,6 +2,7 @@ package org.usvm import io.ksmt.expr.KBitVec32Value import io.ksmt.expr.KConst +import io.ksmt.expr.KFpRoundingMode import io.ksmt.utils.asExpr import org.jacodb.go.api.ArrayType import org.jacodb.go.api.BasicType @@ -81,12 +82,14 @@ 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 @@ -100,40 +103,58 @@ 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.symbolicPrimitiveMapMergeInto import org.usvm.api.collection.PrimitiveMapCollectionApi.symbolicPrimitiveMapRemove -import org.usvm.api.memcpy -import org.usvm.api.readArrayIndex -import org.usvm.api.readArrayLength +import org.usvm.api.makeSymbolicRefUntyped import org.usvm.api.readField import org.usvm.api.refSetContainsElement import org.usvm.api.setContainsElement import org.usvm.api.typeStreamOf -import org.usvm.api.writeArrayIndex import org.usvm.api.writeField import org.usvm.collection.array.UArrayIndexLValue import org.usvm.collection.field.UFieldLValue import org.usvm.collection.map.length.UMapLengthLValue import org.usvm.collection.map.primitive.UMapEntryLValue import org.usvm.collection.map.ref.URefMapEntryLValue +import org.usvm.collection.set.ref.URefSetEntryLValue +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.GoMapKeyRepresentative import org.usvm.state.GoMethodResult -import org.usvm.state.GoState.Companion.POINTER_FIELD +import org.usvm.state.GoResolvedMapKey +import org.usvm.state.copyValue +import org.usvm.state.decodeUtf8Rune +import org.usvm.state.mapKeyCanonicalityConstraint +import org.usvm.state.mapKeyPayloadLValue +import org.usvm.state.mapKeySnapshotLValue +import org.usvm.state.valueShape import org.usvm.statistics.ApplicationGraph import org.usvm.type.GoBasicTypes import org.usvm.type.mapKeyStorageType +import org.usvm.type.needsMapKeyRepresentative import org.usvm.type.underlying import org.usvm.types.first +import org.usvm.util.hasUnsupportedInstructions import org.usvm.util.isInit -import org.usvm.memory.allocateGoArray as allocateArray +import java.util.Base64 +@Suppress("LargeClass") // One visitor implements the SSA expression interface. class GoExprVisitor( private val ctx: GoContext, private val program: GoProgram, @@ -158,39 +179,102 @@ class GoExprVisitor( return result.value } - val args = expr.args.let { if (expr.callee == null) it else listOf(func) + it } + if (expr.callee != null) return callInterface(expr, func) + + val args = expr.args val method = when { - expr.callee != null -> { - val instance = func.accept(this).asExpr(ctx.addressSort) - val type = scope.calcOnState { - scope.assert(memory.types.evalIsSubtype(instance, func.type)) ?: throw IllegalStateException() - memory.typeStreamOf(instance).first() + 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, + ) } - program.findMethod(expr.location, "(${type.typeName}).${expr.callee!!.name}") } - func is GoFunction -> program.findMethod(expr.location, func.metName) - func is GoVar -> scope.calcOnState { - val functionRef = memory.read(URegisterStackLValue(ctx.addressSort, index(func.name))) as KConst - program.findMethod(expr.location, functionRef.decl.name) + else -> { + throw UnknownFunctionException(func.toString()) } - - else -> throw UnknownFunctionException(func.toString()) + } + if (method.hasUnsupportedInstructions()) { + return unsupportedExpr("method ${method.metName} contains unsupported instructions") } if (method.blocks.isEmpty()) { return mockCall(expr, method) } - val parameters = args.map { it.accept(this) }.toTypedArray() - val call = GoCall(method, applicationGraph.entryPoints(method).first()) - ctx.setMethodInfo(method, parameters) - + 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) } @@ -200,7 +284,7 @@ class GoExprVisitor( val lastBlock = scope.calcOnState { var node = pathNode while (node.statement.location.index == currentBlock) { - node = node.parent!! + node = checkNotNull(node.parent) { "Phi has no predecessor in the execution path" } } node.statement.location.index } @@ -266,34 +350,57 @@ class GoExprVisitor( override fun visitGoConvertExpr(expr: GoConvertExpr): UExpr { val value = expr.operand.accept(this) - return when (val type = expr.type) { - GoBasicTypes.UNSAFE_POINTER -> ctx.mkBv(-1L) - GoBasicTypes.STRING -> { - val sliceType = expr.operand.type as SliceType - when (sliceType.elementType) { - GoBasicTypes.RUNE -> ctx.nullRef // TODO - GoBasicTypes.UINT8 -> value - else -> throw IllegalStateException("only []rune and []int can be converted to string") - } + 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 + ) } - - is PointerType -> mkPointer(type) else -> { - when (expr.operand.type) { - GoBasicTypes.UNSAFE_POINTER -> tryBox(ctx.mkBv(-1L), expr.type) - GoBasicTypes.STRING -> { - val sliceType = type as SliceType - when (sliceType.elementType) { - GoBasicTypes.RUNE -> ctx.nullRef // TODO - GoBasicTypes.UINT8 -> value - else -> throw IllegalStateException("string can be converted only to []rune and []int") - } - } - - else -> ctx.mkPrimitiveCast(value, ctx.typeToSort(type)) - } + ctx.mkPrimitiveCast(operand, targetSort) } } + return tryBox(converted, expr.type) } override fun visitGoMultiConvertExpr(expr: GoMultiConvertExpr): UExpr { @@ -308,38 +415,25 @@ class GoExprVisitor( override fun visitGoSliceToArrayPointerExpr(expr: GoSliceToArrayPointerExpr): UExpr { val slice = unboxNamedRef(expr.operand.accept(this).asExpr(ctx.addressSort), expr.operand.type) val sliceType = expr.operand.type.underlying() as SliceType - val sliceLength = scope.calcOnState { memory.readArrayLength(slice, sliceType, ctx.sizeSort) } - + val view = scope.calcOnState { arrayView(slice, sliceType) } val arrayType = (expr.type as PointerType).baseType.underlying() as ArrayType val arrayLength = ctx.mkSizeExpr(arrayType.len.toInt()) - - checkLength(sliceLength) ?: throw IllegalStateException() - checkNotNull(slice) ?: throw IllegalStateException() - checkSliceToArrayPointerLength(sliceLength, arrayLength) ?: throw IllegalStateException() + checkSliceToArrayPointerLength(view.length, arrayLength) ?: throw GoStepAbort() return scope.calcOnState { - val array = memory.allocateArray(arrayType, ctx.sizeSort, arrayLength) - for (i in 0 until arrayType.len) { - val idx = ctx.mkSizeExpr(i.toInt()) - val element = memory.readArrayIndex(slice, idx, sliceType, ctx.typeToSort(sliceType.elementType)) - memory.writeArrayIndex( - array, - idx, - arrayType, - ctx.typeToSort(arrayType.elementType), - element, - ctx.trueExpr, - ) - } - + val array = memory.allocateGoArray(arrayType, ctx.sizeSort, arrayLength) + data.arrayViews[array] = view.copy(length = arrayLength, capacity = arrayLength) tryBox(mkPointer(arrayType, array), expr.type) } } override fun visitGoMakeInterfaceExpr(expr: GoMakeInterfaceExpr): UExpr { - val value = box(expr.value.accept(this@GoExprVisitor), expr.value.type) + 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 IllegalStateException() + scope.assert(memory.types.evalIsSubtype(value, expr.type)) ?: throw GoStepAbort() } return value @@ -350,14 +444,11 @@ class GoExprVisitor( } override fun visitGoMakeMapExpr(expr: GoMakeMapExpr): UExpr { - val reserve = expr.reserve.accept(this).asExpr(ctx.sizeSort) val mapType = expr.type.underlying() - checkLength(reserve) ?: throw IllegalStateException() - return scope.calcOnState { val ref = memory.allocConcrete(mapType) - memory.write(UMapLengthLValue(ref, mapType, ctx.sizeSort), reserve, ctx.trueExpr) + memory.write(UMapLengthLValue(ref, mapType, ctx.sizeSort), ctx.mkSizeExpr(0), ctx.trueExpr) tryBox(ref.asExpr(ctx.addressSort), expr.type) } } @@ -368,59 +459,71 @@ class GoExprVisitor( } override fun visitGoMakeSliceExpr(expr: GoMakeSliceExpr): UExpr { - val len = expr.len.accept(this).asExpr(ctx.sizeSort) + val length = collectionSize(expr.len) + val capacity = collectionSize(expr.cap) - checkLength(len) ?: throw IllegalStateException() + checkLength(length) ?: throw GoStepAbort() + checkLength(capacity) ?: throw GoStepAbort() + checkSliceBound(length, capacity) ?: throw GoStepAbort() return scope.calcOnState { - tryBox(memory.allocateArray(expr.type.underlying(), ctx.sizeSort, len), expr.type) + 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 unboxedArray = unboxNamedRef(boxedArray, expr.array.type) - val array = if (isPointerConcrete(unboxedArray)) deref(unboxedArray, ctx.addressSort) else unboxedArray - val arrayType = expr.array.type.let { if (it is PointerType) it.baseType else it }.underlying() - val sliceType = expr.type.underlying() - val length = scope.calcOnState { memory.readArrayLength(array, arrayType, ctx.sizeSort) } - val low = expr.low.accept(this).asExpr(ctx.sizeSort) - val suppliedHigh = expr.high.accept(this) - val hasDefaultHigh = suppliedHigh is KBitVec32Value && suppliedHigh.intValue == -1 - val high = (if (hasDefaultHigh) length else suppliedHigh).asExpr(ctx.sizeSort) + val (array, arrayType) = resolveArrayInstance(expr.array) + 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) - val elementType = when (arrayType) { - is ArrayType -> arrayType.elementType - is SliceType -> arrayType.elementType - GoBasicTypes.STRING -> GoBasicTypes.UINT8 - else -> throw IllegalStateException("illegal type for collection") - } - val elementSort = ctx.typeToSort(elementType) - checkLength(length) ?: throw IllegalStateException() - checkNotNull(array) ?: throw IllegalStateException() - checkNegativeIndex(low) ?: throw IllegalStateException() - checkNegativeIndex(high) ?: throw IllegalStateException() - checkIndexOutOfBounds(low, ctx.mkSizeAddExpr(high, ctx.mkSizeExpr(1))) ?: throw IllegalStateException() - checkIndexOutOfBounds(high, ctx.mkSizeAddExpr(length, ctx.mkSizeExpr(1))) ?: throw IllegalStateException() + 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) + } - return scope.calcOnState { - val result = memory.allocateArray(sliceType, ctx.sizeSort, count) - when (arrayType) { - is ArrayType -> { - val tempArray = memory.allocateArray(sliceType, ctx.sizeSort, count) - for (i in 0 until arrayType.len) { - val idx = ctx.mkSizeExpr(i.toInt()) - val element = memory.readArrayIndex(array, idx, arrayType, elementSort) - memory.writeArrayIndex(tempArray, idx, sliceType, elementSort, element, ctx.trueExpr) - } - memory.memcpy(tempArray, result, sliceType, elementSort, low, ctx.mkSizeExpr(0), count) - } + private fun sliceBound(value: GoValue, default: UExpr): UExpr { + return if (value is GoNullConstant) default else collectionSize(value) + } - else -> memory.memcpy(array, result, sliceType, elementSort, low, ctx.mkSizeExpr(0), count) - } - tryBox(result, expr.type) + 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 { @@ -432,70 +535,82 @@ class GoExprVisitor( } val pointer = expr.instance.accept(this).asExpr(ctx.addressSort) - val struct = deref(pointer, ctx.addressSort) - checkNotNull(struct) ?: throw IllegalStateException() + 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) + val fieldType = (expr.type as PointerType).baseType + val fieldLValue = UFieldLValue(ctx.typeToSort(fieldType), struct, expr.field) + return mkPointer(fieldType, fieldLValue) } override fun visitGoFieldExpr(expr: GoFieldExpr): UExpr { - val struct = expr.instance.accept(this).asExpr(ctx.addressSort) - checkNotNull(struct) ?: throw IllegalStateException() + 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 (array, index) = visitIndexExpr(expr.instance, expr.index) - val arrayType = expr.instance.type.let { if (it is PointerType) it.baseType else it }.underlying() + val (view, index) = visitIndexExpr(expr.instance, expr.index) val elementType = (expr.type as PointerType).baseType - val elementLValue = UArrayIndexLValue(ctx.typeToSort(elementType), array, index, arrayType) + val elementLValue = UArrayIndexLValue(ctx.typeToSort(elementType), view.backing, index, view.storageType) return mkPointer(elementType, elementLValue) } override fun visitGoIndexExpr(expr: GoIndexExpr): UExpr { - val (array, index) = visitIndexExpr(expr.instance, expr.index) - val arrayType = expr.instance.type.let { if (it is PointerType) it.baseType else it }.underlying() + val (view, index) = visitIndexExpr(expr.instance, expr.index) return scope.calcOnState { - memory.readArrayIndex(array, index, arrayType, ctx.typeToSort(expr.type)) + memory.readGoArrayIndex(view.backing, index, view.storageType, ctx.typeToSort(expr.type)) } } override fun visitGoLookupExpr(expr: GoLookupExpr): UExpr { - val map = unboxNamedRef(expr.instance.accept(this).asExpr(ctx.addressSort), expr.instance.type) val mapType = expr.instance.type.underlying() as MapType - val key = resolveMapKey(expr.index) + val reference = unboxNamedRef(expr.instance.accept(this).asExpr(ctx.addressSort), expr.instance.type) + val map = mapOrEmpty(reference, mapType) + val resolvedKey = resolveMapKey(expr.index) + val key = resolvedKey.key val isRefKey = key.sort == ctx.addressSort val commaOk = expr.commaOk val valueSort = ctx.typeToSort(mapType.valueType) - checkNotNull(map) ?: throw IllegalStateException() - scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw IllegalStateException() + scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw GoStepAbort() + ensureMapKeyCanonicality(map, mapType) - val contains = scope.calcOnState { + val canMatch = mapKeyCanMatch(key, mapType.keyType) + val rawContains = scope.calcOnState { if (isRefKey) { memory.refSetContainsElement(map, key.asExpr(ctx.addressSort), mapType) } else { memory.setContainsElement(map, key, mapType, USizeExprKeyInfo()) } } + val contains = ctx.mkAnd(canMatch, rawContains) val lvalue = if (isRefKey) { URefMapEntryLValue(valueSort, map, key.asExpr(ctx.addressSort), mapType) } else { UMapEntryLValue(key.sort, valueSort, map, key.asExpr(key.sort), mapType, USizeExprKeyInfo()) } - val rvalue = scope.calcOnState { memory.read(lvalue).asExpr(valueSort) } + if (mapType.keyType.needsMapKeyRepresentative()) { + validateStoredMapKey(map, key.asExpr(ctx.addressSort), mapType, guard = rawContains) + } + 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)), rvalue, contains) + mkTuple(TupleType(listOf(mapType.valueType, GoBasicTypes.BOOL)), value, contains) } else { - ctx.mkIte(contains, { rvalue }, { rvalue.sort.sampleUValue() }) + value } } } @@ -506,124 +621,190 @@ class GoExprVisitor( } override fun visitGoRangeExpr(expr: GoRangeExpr): UExpr { - val collection = expr.instance.accept(this).asExpr(ctx.addressSort).let { - when (val type = expr.instance.type.underlying()) { - is MapType -> copyMap(it, type) - GoBasicTypes.STRING -> it - else -> throw IllegalStateException("illegal type for range") - } + val reference = unboxNamedRef(expr.instance.accept(this).asExpr(ctx.addressSort), expr.instance.type) + val type = expr.instance.type.underlying() + val collection = if (type is MapType) mapOrEmpty(reference, type) else reference + val remaining = when (type) { + is MapType -> copyMap(collection, type) + GoBasicTypes.STRING -> collection + else -> error("illegal type for range") } + return scope.calcOnState { mkTuple( - TupleType(listOf(expr.instance.type, GoBasicTypes.INT32)), - collection, + TupleType(listOf(expr.instance.type, GoBasicTypes.INT32, expr.instance.type)), + remaining, ctx.mkSizeExpr(0), + collection, ) } } override fun visitGoNextExpr(expr: GoNextExpr): UExpr { val iter = expr.instance.accept(this).asExpr(ctx.addressSort) - return scope.calcOnState { - val tupleType = memory.typeStreamOf(iter).commonSuperType as TupleType - val collection = memory.readField(iter, 0, ctx.addressSort) - val notNull = ctx.mkNot(ctx.mkHeapRefEq(collection, ctx.nullRef)) - when (val collectionType = tupleType.types[0].underlying()) { - GoBasicTypes.STRING -> { - val index = memory.readField(iter, 1, ctx.sizeSort) - val char = memory.readArrayIndex(collection, index, collectionType, ctx.bv8Sort) - val length = memory.readArrayLength(collection, collectionType, ctx.sizeSort) - val ok = ctx.mkAnd(notNull, ctx.mkBvSignedLessExpr(index, length)) - - checkLength(length) ?: throw IllegalStateException() - - memory.writeField(iter, 1, ctx.sizeSort, ctx.mkBvAddExpr(index, ctx.mkSizeExpr(1)), ctx.trueExpr) - mkTuple( - TupleType(listOf(GoBasicTypes.BOOL, GoBasicTypes.INT32, GoBasicTypes.INT32)), - ok, - index, - ctx.mkPrimitiveCast(char, ctx.bv32Sort), - ) - } - - is MapType -> { - scope.ensureObjectMapSizeCorrect(collection, collectionType) ?: throw IllegalStateException() - - val length = symbolicObjectMapSize(collection, collectionType) - val ok = ctx.mkAnd(notNull, ctx.mkBvSignedGreaterExpr(length, ctx.mkBv(0))) - val keySort = ctx.typeToSort(collectionType.keyType.mapKeyStorageType()) - val isPrimitiveKey = keySort != ctx.addressSort - val (key, value) = if (isPrimitiveKey) { - val k = symbolicPrimitiveMapAnyKey(collection, collectionType, keySort, USizeExprKeyInfo()) - val v = symbolicPrimitiveMapGet( - collection, - k, - collectionType, - ctx.typeToSort(collectionType.valueType), - USizeExprKeyInfo(), - ) - 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, - if (isPrimitiveKey) tryBox(key, collectionType.keyType) else key, - value, - ) - } - - else -> throw IllegalStateException("invalid collection type in next expr") + val tupleType = scope.calcOnState { memory.typeStreamOf(iter).commonSuperType as TupleType } + val collection = scope.calcOnState { memory.readField(iter, 0, ctx.addressSort) } + val notNull = ctx.mkNot(ctx.mkHeapRefEq(collection, ctx.nullRef)) + + return when (val collectionType = tupleType.types[0].underlying()) { + GoBasicTypes.STRING -> scope.calcOnState { + val index = memory.readField(iter, 1, ctx.sizeSort) + val length = memory.readGoArrayLength(collection, collectionType, ctx.sizeSort) + val ok = ctx.mkAnd(notNull, ctx.mkSizeLtExpr(index, length)) + checkLength(length) ?: throw GoStepAbort() + val (rune, width) = decodeUtf8Rune(collection, index, length) + + memory.writeField(iter, 1, ctx.sizeSort, ctx.mkSizeAddExpr(index, width), guard = ok) + // SSA marks discarded range components as invalid, but the iterator still yields an int and rune. + val resultType = TupleType(listOf(GoBasicTypes.BOOL, GoBasicTypes.INT, GoBasicTypes.RUNE)) + val goIndex = ctx.mkPrimitiveCast(index, ctx.typeToSort(GoBasicTypes.INT)) + mkTuple(resultType, ok, goIndex, rune) } + is MapType -> nextMapEntry(iter, collection, collectionType) + else -> error("invalid collection type in next expr") } } - override fun visitGoTypeAssertExpr(expr: GoTypeAssertExpr): UExpr { - val x = expr.instance.accept(this) - - val assertType = expr.assertType.let { if (it is TupleType) it.types[0] else it } - val assertSort = ctx.typeToSort(assertType) - - val commaOk = expr.type is TupleType - val tupleType = TupleType(listOf(assertType, GoBasicTypes.BOOL)) - - val ite: (UHeapRef, UExpr, UExpr) -> UExpr = { ref, ok, fail -> - scope.calcOnState { - return@calcOnState ctx.mkIte( - memory.types.evalIsSupertype(ref, assertType), - trueBranch = { ok.asExpr(ok.sort) }, - falseBranch = { fail.asExpr(fail.sort).also { if (!commaOk) panic("type assertion failed") } } - ) + private fun nextMapEntry(iter: UHeapRef, remaining: UHeapRef, type: MapType): UHeapRef { + scope.ensureObjectMapSizeCorrect(remaining, type) ?: throw GoStepAbort() + val original = scope.calcOnState { memory.readField(iter, 2, ctx.addressSort) } + ensureMapKeyCanonicality(original, type) + ensureMapKeyCanonicality(remaining, type) + val length = scope.calcOnState { symbolicObjectMapSize(remaining, type) } + val hasNext = ctx.mkSizeGtExpr(length, ctx.mkSizeExpr(0)) + val keySort = ctx.mapKeySort(type.keyType) + val valueSort = ctx.typeToSort(type.valueType) + val isReferenceKey = keySort == ctx.addressSort + val key = scope.calcOnState { + if (isReferenceKey) { + symbolicObjectMapAnyKey(remaining, type) + } else { + symbolicPrimitiveMapAnyKey(remaining, type, keySort, USizeExprKeyInfo()) + } + } + if (type.keyType.needsMapKeyRepresentative()) { + val snapshot = copyMapRangeKey(original, key.asExpr(ctx.addressSort), type, guard = hasNext) + ensureMapKeyComparable(snapshot, type.keyType, guard = hasNext, panicOnFailure = false) + val canonicalSnapshot = scope.calcOnState { + memory.read(ctx.mapKeySnapshotLValue(key.asExpr(ctx.addressSort), type.keyType)) + } + val matchesStorage = valuesEqual( + snapshot, + canonicalSnapshot, + type.keyType, + guard = hasNext, + representationEquality = true, + ) + assertMapKeyConstraint(ctx.mkOr(ctx.mkNot(hasNext), matchesStorage)) + val previous = scope.calcOnState { data.mapKeyRepresentatives[type.keyType].orEmpty() } + for (entry in previous) { + val active = ctx.mkAnd(hasNext, entry.guard) + val equal = valuesEqual(snapshot, entry.snapshot, type.keyType, guard = active) + val sameRepresentative = ctx.mkHeapRefEq(key.asExpr(ctx.addressSort), entry.canonicalReference) + assertMapKeyConstraint(ctx.mkOr(ctx.mkNot(ctx.mkAnd(active, equal)), sameRepresentative)) + } + scope.doWithState { + data.mapKeyRepresentatives[type.keyType] = previous + + GoMapKeyRepresentative(key.asExpr(ctx.addressSort), snapshot, guard = hasNext) + } + } + val wasPresent = scope.calcOnState { + if (isReferenceKey) { + memory.refSetContainsElement(remaining, key.asExpr(ctx.addressSort), type) + } else { + memory.setContainsElement(remaining, key, type, USizeExprKeyInfo()) } } + assertMapKeyConstraint(ctx.mkOr(ctx.mkNot(hasNext), wasPresent)) - val xAddr = x.asExpr(ctx.addressSort) - checkNotNull(xAddr) ?: throw IllegalStateException() - val unboxedValue = unbox(xAddr, assertSort) + val stillPresent = scope.calcOnState { + if (isReferenceKey) { + symbolicObjectMapRemove(remaining, key.asExpr(ctx.addressSort), type) + memory.refSetContainsElement(original, key.asExpr(ctx.addressSort), type) + } else { + symbolicPrimitiveMapRemove(remaining, key, type, USizeExprKeyInfo()) + memory.setContainsElement(original, key, type, USizeExprKeyInfo()) + } + } + // A deleted key consumes one machine step. The false successor retries this SSA instruction. + val canYieldOrFinish = ctx.mkOr(ctx.mkNot(hasNext), stillPresent) + scope.fork(canYieldOrFinish) ?: throw GoStepAbort() return scope.calcOnState { - val sample = if (assertSort == ctx.addressSort) { - ctx.nullRef + val value = if (isReferenceKey) { + symbolicObjectMapGet(original, key.asExpr(ctx.addressSort), type, valueSort) } else { - assertSort.sampleUValue().asExpr(assertSort) + symbolicPrimitiveMapGet(original, key, type, valueSort, USizeExprKeyInfo()) } - if (commaOk) { - mkTuple(tupleType, ite(xAddr, unboxedValue, sample), ite(xAddr, ctx.trueExpr, ctx.falseExpr)) + mkTuple( + TupleType(listOf(GoBasicTypes.BOOL, type.keyType, type.valueType)), + hasNext, + if (type.keyType.needsMapKeyRepresentative()) { + copyMapRangeKey(original, key.asExpr(ctx.addressSort), type, guard = hasNext) + } else { + tryBox(key, type.keyType) + }, + ctx.mkIte( + hasNext, + trueBranch = { copyValue(value, type.valueType).asExpr(valueSort) }, + falseBranch = { sampleValue(type.valueType).asExpr(valueSort) }, + ), + ) + } + } + + private fun copyMapRangeKey( + map: UHeapRef, + key: UHeapRef, + type: MapType, + guard: UBoolExpr, + ): UExpr = scope.calcOnState { + val sort = ctx.typeToSort(type.keyType) + val stored = memory.read(ctx.mapKeyPayloadLValue(map, key, type)) + val initialized = ctx.mkIte( + guard, + trueBranch = { stored.asExpr(sort) }, + falseBranch = { sampleValue(type.keyType).asExpr(sort) }, + ) + copyValue(initialized, type.keyType) + } + + private fun assertMapKeyConstraint(constraint: UBoolExpr) { + scope.assert(constraint) ?: throw GoStepAbort() + } + + 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 { - ite(xAddr, unboxedValue, sample) + 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 { @@ -642,15 +823,19 @@ class GoExprVisitor( override fun visitGoFreeVar(expr: GoFreeVar): UExpr { return scope.calcOnState { - val registerIndex = expr.index + ctx.freeVariableOffset(lastEnteredMethod) - memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), registerIndex)) + memory.read( + URegisterStackLValue(ctx.typeToSort(expr.type), expr.index + ctx.freeVariableOffset(lastEnteredMethod)) + ) } } override fun visitGoParameter(expr: GoParameter): UExpr { - return scope.calcOnState { + val value = scope.calcOnState { memory.read(URegisterStackLValue(ctx.typeToSort(expr.type), expr.index)) } + ensureNamedScalar(value, expr.type) + + return value } override fun visitGoConst(expr: GoConst): UExpr { @@ -678,7 +863,7 @@ class GoExprVisitor( } override fun visitGoInt(value: GoInt): UExpr = with(ctx) { - return tryBox(mkBv(value.value.toInt()), value.type) + return tryBox(mkBv(value.value.toLong(), typeToSort(value.type.underlying()) as UBvSort), value.type) } override fun visitGoInt8(value: GoInt8): UExpr = with(ctx) { @@ -698,7 +883,7 @@ class GoExprVisitor( } override fun visitGoUInt(value: GoUInt): UExpr = with(ctx) { - return tryBox(mkBv(value.value.toInt()), value.type) + return tryBox(mkBv(value.value.toLong(), typeToSort(value.type.underlying()) as UBvSort), value.type) } override fun visitGoUInt8(value: GoUInt8): UExpr = with(ctx) { @@ -710,7 +895,7 @@ class GoExprVisitor( } override fun visitGoUInt32(value: GoUInt32): UExpr = with(ctx) { - return tryBox(mkBv(value.value.toInt()), value.type) + return tryBox(mkBv(value.value.toLong(), typeToSort(value.type.underlying()) as UBvSort), value.type) } override fun visitGoUInt64(value: GoUInt64): UExpr = with(ctx) { @@ -725,11 +910,18 @@ class GoExprVisitor( return tryBox(mkFp(value.value, fp64Sort), value.type) } - override fun visitGoNullConstant(value: GoNullConstant): UExpr = ctx.nullRef + override fun visitGoNullConstant(value: GoNullConstant): UExpr { + val type = value.type.underlying() + return if (type is ArrayType || type is org.jacodb.go.api.StructType) { + scope.calcOnState { sampleValue(value.type) } + } else { + ctx.nullRef + } + } override fun visitGoStringConstant(value: GoStringConstant): UExpr { return scope.calcOnState { - tryBox(mkString(value.value), value.type) + tryBox(mkString(Base64.getDecoder().decode(value.value)), value.type) } } @@ -740,46 +932,81 @@ class GoExprVisitor( } fun unboxNamedRef(expr: UHeapRef, type: GoType): UHeapRef { - if (type !is NamedType) { + if (type !is NamedType || type.underlying() is InterfaceType) { return expr } - return unbox(expr, ctx.typeToSort(type.underlying())).asExpr(ctx.addressSort) + 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 } + ) + private fun visitGoBinaryExpr(expr: GoBinaryExpr): UExpr { val bothStrings = expr.lhv.type.underlying() == GoBasicTypes.STRING && expr.rhv.type.underlying() == GoBasicTypes.STRING if (bothStrings) { - return tryBox(appendArray(expr.lhv, expr.rhv), expr.type) + 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 signed = expr.lhv.type.underlying() is BasicType && !expr.lhv.type.underlying().typeName.startsWith("ui") + 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 + val usesReferenceEquality = isEquality && bothReferences && (hasNilOperand || pointerOperands) + if (usesReferenceEquality) { + 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 hasValueEquality = operandType is StructType || operandType is ArrayType || operandType is InterfaceType + val usesValueEquality = isEquality && hasValueEquality + if (usesValueEquality) { + val equal = valuesEqual(lhv, rhv, expr.lhv.type, guard = ctx.trueExpr) + return if (expr is GoEqlExpr) equal else ctx.mkNot(equal) + } + + val signed = operandType is BasicType && !operandType.typeName.startsWith("uint") val lhs = unboxNamedPrimitive(lhv, expr.lhv.type) val rhs = unboxNamedPrimitive(rhv, expr.rhv.type) - val result = when (expr) { - is GoAddExpr -> GoBinaryOperator.Add - is GoSubExpr -> GoBinaryOperator.Sub - is GoMulExpr -> GoBinaryOperator.Mul - is GoDivExpr -> GoBinaryOperator.Div(signed) - is GoModExpr -> GoBinaryOperator.Mod(signed) - is GoAndExpr -> GoBinaryOperator.And - is GoOrExpr -> GoBinaryOperator.Or - is GoXorExpr -> GoBinaryOperator.Xor - is GoShlExpr -> GoBinaryOperator.Shl - is GoShrExpr -> GoBinaryOperator.Shr(signed) - is GoAndNotExpr -> GoBinaryOperator.AndNot - is GoEqlExpr -> GoBinaryOperator.Eql - is GoLssExpr -> GoBinaryOperator.Lss(signed) - is GoGtrExpr -> GoBinaryOperator.Gtr(signed) - is GoNeqExpr -> GoBinaryOperator.Neq - is GoLeqExpr -> GoBinaryOperator.Leq(signed) - is GoGeqExpr -> GoBinaryOperator.Geq(signed) - else -> throw UnknownBinaryOperationException(expr.toString()) - }(lhs, normalize(lhs, rhs, expr, signed)) + val integerDivision = (expr is GoDivExpr || expr is GoModExpr) && rhs.sort is UBvSort + if (integerDivision) { + 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 operator = binaryOperator(expr, signed) + val result = operator(lhs, normalize(lhs, rhs, expr)) if (expr.type is NamedType) { return box(result, expr.type) @@ -787,47 +1014,134 @@ class GoExprVisitor( return result } - private fun visitGoUnaryExpr(expr: GoUnaryExpr): UExpr { - val x = expr.value.accept(this) + private fun binaryOperator(expr: GoBinaryExpr, signed: Boolean): GoBinaryOperator { return when (expr) { - is GoUnArrowExpr -> throw UnsupportedUnaryOperationException("channel operations") - is GoUnXorExpr, is GoUnNotExpr, is GoUnSubExpr -> GoUnaryOperator.Neg(x) - is GoUnMulExpr -> deref(x, ctx.typeToSort(expr.type)) - else -> throw UnknownUnaryOperationException(expr.toString()) + 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()) + } } } - private fun visitIndexExpr(instance: GoValue, idx: GoValue): Pair> { - val unboxedArray = unboxNamedRef(instance.accept(this).asExpr(ctx.addressSort), instance.type) - val array = if (isPointerConcrete(unboxedArray)) deref(unboxedArray, ctx.addressSort) else unboxedArray - val arrayType = instance.type.let { if (it is PointerType) it.baseType else it }.underlying() - val indexValue = bv(unboxNamedPrimitive(idx.accept(this), idx.type)) - val index = indexValue.mkNarrow(Int.SIZE_BITS, signed = false).asExpr(ctx.sizeSort) - val length = scope.calcOnState { memory.readArrayLength(array, arrayType, ctx.sizeSort) } + 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) + } - checkLength(length) ?: throw IllegalStateException() - checkNotNull(array) ?: throw IllegalStateException() - checkNegativeIndex(index) ?: throw IllegalStateException() - checkIndexOutOfBounds(index, length) ?: throw IllegalStateException() + private fun resolveArrayInstance(instance: GoExpr): Pair { + val reference = instance.accept(this).asExpr(ctx.addressSort) + val instanceType = instance.type.underlying() + val unboxed = unboxNamedRef(reference, instance.type) + if (instanceType is PointerType) { + val value = deref(unboxed, ctx.addressSort) + val array = unboxNamedRef(value, instanceType.baseType) + return array to instanceType.baseType.underlying() + } - return array to index + return unboxed to instanceType } - private fun normalize( - lhs: UExpr, - rhs: UExpr, - goExpr: GoBinaryExpr, - signed: Boolean, - ): UExpr = with(ctx) { - when (goExpr) { - is GoShrExpr -> { - val differentWidths = bv(lhs).sort.sizeBits != bv(rhs).sort.sizeBits - if (differentWidths) bv(rhs).mkNarrow(Long.SIZE_BITS, signed).asExpr(bv64Sort) else rhs - } - else -> rhs + private fun visitIndexExpr(instance: GoValue, idx: GoValue): Pair> { + val (array, arrayType) = resolveArrayInstance(instance) + 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) } @@ -836,17 +1150,6 @@ class GoExprVisitor( 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) } } @@ -861,8 +1164,8 @@ class GoExprVisitor( private fun deref(expr: UExpr, sort: Sort): UExpr = with(ctx) { val pointer = expr.asExpr(addressSort) - checkIsPointer(pointer) ?: throw IllegalStateException() - checkNotNull(pointer) ?: throw IllegalStateException() + scope.assert(ctx.mkOr(ctx.mkHeapRefEq(pointer, nullRef), isPointer(pointer))) ?: throw GoStepAbort() + checkNotNull(pointer) ?: throw GoStepAbort() return scope.calcOnState { deref(pointer, sort).asExpr(sort) } @@ -875,43 +1178,123 @@ class GoExprVisitor( } private fun unbox(expr: UHeapRef, sort: USort): UExpr { - checkIsBoxed(expr) ?: throw IllegalStateException() - checkNotNull(expr) ?: throw IllegalStateException() + 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) { + if (type !is NamedType || type.underlying() is InterfaceType) { return expr } - return unbox(expr.asExpr(ctx.addressSort), ctx.typeToSort(type.underlying())) + 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) } } - internal fun resolveMapKey(value: GoExpr): UExpr { + internal fun checkMapWritable(map: UHeapRef): Unit? = scope.fork( + ctx.mkNot(ctx.mkHeapRefEq(map, ctx.nullRef)), + blockOnFalseState = { panic("assignment to entry in nil map") }, + ) + + internal fun evaluateMapKey(value: GoExpr): UExpr { val key = value.accept(this) - val storageType = value.type.mapKeyStorageType() - if (storageType == value.type) return key + return scope.calcOnState { copyValue(key, value.type) } + } + + internal fun resolveMapKey(value: GoValue): GoResolvedMapKey = + resolveMapKeySnapshot(evaluateMapKey(value), value.type) + + internal fun resolveMapKeySnapshot(snapshot: UExpr, type: GoType): GoResolvedMapKey { + val needsRepresentative = type.needsMapKeyRepresentative() + if (!needsRepresentative) { + val storageType = type.mapKeyStorageType() + val storageKey = if (storageType == type) snapshot else unboxNamedPrimitive(snapshot, type) + return GoResolvedMapKey(storageKey, snapshot) + } - return unboxNamedPrimitive(key, value.type) + ensureMapKeyComparable(snapshot, type, guard = ctx.trueExpr, panicOnFailure = true) + val previous = scope.calcOnState { data.mapKeyRepresentatives[type].orEmpty() } + val fresh = scope.calcOnState { makeSymbolicRefUntyped() } + val freshIdentity = ctx.mkAnd( + listOf(ctx.mkNot(ctx.mkHeapRefEq(fresh, ctx.nullRef))) + previous.map { entry -> + ctx.mkNot(ctx.mkHeapRefEq(fresh, entry.reference)) + }, + ) + scope.assert(freshIdentity) ?: throw GoStepAbort() + scope.doWithState { + val lvalue = ctx.mapKeySnapshotLValue(fresh, type) + memory.write(lvalue, snapshot.asExpr(lvalue.sort), guard = ctx.trueExpr) + } + + var representative: UHeapRef = fresh + for (entry in previous.asReversed()) { + val equal = ctx.mkAnd(entry.guard, valuesEqual(snapshot, entry.snapshot, type, guard = entry.guard)) + representative = ctx.mkIte(equal, entry.canonicalReference, representative) + } + scope.doWithState { + data.mapKeyRepresentatives[type] = previous + GoMapKeyRepresentative( + fresh, snapshot, guard = ctx.trueExpr, canonicalReference = representative, + ) + } + return GoResolvedMapKey(representative, snapshot) + } + + internal fun ensureMapKeyCanonicality(map: UHeapRef, type: MapType) { + val canonicality = scope.calcOnState { mapKeyCanonicalityConstraint(map, type) } + scope.assert(canonicality) ?: throw GoStepAbort() + } + + internal fun validateStoredMapKey(map: UHeapRef, key: UHeapRef, type: MapType, guard: UBoolExpr) { + val stored = scope.calcOnState { memory.read(ctx.mapKeyPayloadLValue(map, key, type)) } + val canonical = scope.calcOnState { memory.read(ctx.mapKeySnapshotLValue(key, type.keyType)) } + val matches = valuesEqual(stored, canonical, type.keyType, guard, representationEquality = true) + scope.assert(ctx.mkOr(ctx.mkNot(guard), matches)) ?: throw GoStepAbort() + } + + internal fun mapKeyCanMatch(key: UExpr, type: GoType): UBoolExpr { + val needsRepresentative = type.needsMapKeyRepresentative() + if (!needsRepresentative) return ctx.trueExpr + + val snapshot = scope.calcOnState { memory.read(ctx.mapKeySnapshotLValue(key.asExpr(ctx.addressSort), type)) } + return valuesEqual(snapshot, snapshot, type, guard = ctx.trueExpr) } private fun tryBox(expr: UExpr, targetType: GoType): UExpr { - return if (targetType is NamedType) box(expr, targetType) else expr + return if (targetType is NamedType && targetType.underlying() !is InterfaceType) box(expr, targetType) else expr } private fun index(name: String): Int { return name.substring(1).toInt() + ctx.localVariableOffset(scope.calcOnState { lastEnteredMethod }) } - private fun checkIndexOutOfBounds(index: UExpr, length: UExpr): Unit? = with(ctx) { - scope.fork(mkSizeLtExpr(index, length), blockOnFalseState = { - panic("index out of bounds") - }) + private fun referenceEquals(left: UHeapRef, right: UHeapRef): UExpr = scope.calcOnState { + ctx.mkHeapRefEq(canonicalPointer(left), canonicalPointer(right)) } + private fun checkSliceBound(bound: UExpr, limit: UExpr): Unit? = + scope.fork(ctx.mkSizeLeExpr(bound, limit), blockOnFalseState = { + panic("slice bounds out of range") + }) + private fun checkNegativeIndex(value: UExpr): Unit? = with(ctx) { scope.fork(mkSizeGeExpr(value, mkSizeExpr(0)), blockOnFalseState = { panic("negative index") @@ -924,52 +1307,61 @@ class GoExprVisitor( private fun checkSliceToArrayPointerLength( sliceLength: UExpr, - arrayLength: UExpr + arrayLength: UExpr, ): Unit? = with(ctx) { scope.fork(mkSizeGeExpr(sliceLength, arrayLength), blockOnFalseState = { panic("length of the slice is less than the length of the array") }) } - private fun checkIsPointer(obj: UHeapRef): Unit? = scope.fork(isPointer(obj), blockOnFalseState = { - panic("not a pointer") - }) - private fun checkIsBoxed(obj: UHeapRef): Unit? = scope.fork(isBoxed(obj), blockOnFalseState = { panic("not a boxed value") }) + @Suppress("ThrowsCount") // Failed scope checks stop this hop. private fun callBuiltin(method: GoBuiltin, args: List, returnType: GoType): UExpr { + val completed = scope.calcOnState { + data.builtinResult?.also { data.builtinResult = null } + } + if (completed != null) return tryBox(completed, returnType) + return when (method.name) { - "append" -> tryBox(appendArray(args[0], args[1]), returnType) + "append" -> { + val result = appendArray(args[0], args[1]) + if (result == ctx.noValue) result else tryBox(result, returnType) + } "copy" -> { - val src = unboxNamedRef(args[1].accept(this).asExpr(ctx.addressSort), args[1].type) - val dst = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) - val sliceType = args[0].type.underlying() as SliceType - val elementSort = ctx.typeToSort(sliceType.elementType) - - return scope.calcOnState { - val srcLength = memory.readArrayLength(src, sliceType, ctx.sizeSort) - val dstLength = memory.readArrayLength(dst, sliceType, ctx.sizeSort) - checkLength(srcLength) ?: throw IllegalStateException() - checkLength(dstLength) ?: throw IllegalStateException() - val numCopied = ctx.mkIte(ctx.mkSizeLtExpr(srcLength, dstLength), srcLength, dstLength) - - memory.memcpy(src, dst, sliceType, elementSort, ctx.mkSizeExpr(0), ctx.mkSizeExpr(0), numCopied) - numCopied - } + copyArray(args, returnType) } "delete" -> { - val map = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) - val key = resolveMapKey(args[1]) + val reference = unboxNamedRef(args[0].accept(this).asExpr(ctx.addressSort), args[0].type) + val resolvedKey = resolveMapKey(args[1]) + val key = resolvedKey.key val mapType = args[0].type.underlying() as MapType - val keySort = ctx.typeToSort(mapType.keyType.mapKeyStorageType()) - - scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw IllegalStateException() + val map = mapOrEmpty(reference, mapType) + val keySort = ctx.mapKeySort(mapType.keyType) + + scope.ensureObjectMapSizeCorrect(map, mapType) ?: throw GoStepAbort() + ensureMapKeyCanonicality(map, mapType) + val canMatch = mapKeyCanMatch(key, mapType.keyType) + if (mapType.keyType.needsMapKeyRepresentative()) { + val rawContains = scope.calcOnState { + memory.refSetContainsElement(map, key.asExpr(ctx.addressSort), mapType) + } + validateStoredMapKey(map, key.asExpr(ctx.addressSort), mapType, guard = rawContains) + } scope.doWithState { if (keySort == ctx.addressSort) { - symbolicObjectMapRemove(map, key.asExpr(ctx.addressSort), mapType) + val membership = URefSetEntryLValue(map, key.asExpr(ctx.addressSort), mapType) + val wasPresent = ctx.mkAnd(canMatch, memory.read(membership)) + val length = symbolicObjectMapSize(map, mapType) + memory.write(membership, ctx.falseExpr, canMatch) + memory.write( + UMapLengthLValue(map, mapType, ctx.sizeSort), + ctx.mkSizeSubExpr(length, ctx.mkSizeExpr(1)), + wasPresent, + ) } else { symbolicPrimitiveMapRemove(map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) } @@ -981,29 +1373,36 @@ class GoExprVisitor( val arg = args[0] val collection = unboxNamedRef(arg.accept(this).asExpr(ctx.addressSort), arg.type) - return ctx.mkIte( - ctx.mkNot(ctx.mkHeapRefEq(collection, ctx.nullRef)), - trueBranch = { - scope.calcOnState { - when (val type = arg.type.underlying()) { - is ArrayType, is SliceType, is BasicType -> { - val length = memory.readArrayLength(collection, type, ctx.sizeSort) - checkLength(length) ?: throw IllegalStateException() - length + 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() + ensureMapKeyCanonicality(collection, type) + symbolicObjectMapSize(collection, type) + } + + else -> { + error("Inconsistent Go interpreter state") + } } - - is MapType -> { - scope.ensureObjectMapSizeCorrect(collection, type) ?: throw IllegalStateException() - symbolicObjectMapSize(collection, type) - } - - else -> throw IllegalStateException() } - } - }, - falseBranch = { - ctx.mkSizeExpr(0) - }, + }, + falseBranch = { + ctx.mkSizeExpr(0) + }, + ), + ctx.typeToSort(returnType) ) } @@ -1015,11 +1414,49 @@ class GoExprVisitor( } } - "recover" -> scope.calcOnState { - recover() + "recover" -> { + scope.calcOnState { + recover() + } } - else -> ctx.nullRef + 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 + } } } @@ -1027,56 +1464,426 @@ class GoExprVisitor( srcMap: UHeapRef, mapType: MapType, ): UHeapRef = with(ctx) { - checkNotNull(srcMap) ?: throw IllegalStateException() - scope.ensureObjectMapSizeCorrect(srcMap, mapType) ?: throw IllegalStateException() + val source = mapOrEmpty(srcMap, mapType) + scope.ensureObjectMapSizeCorrect(source, mapType) ?: throw GoStepAbort() + ensureMapKeyCanonicality(source, mapType) - val keySort = typeToSort(mapType.keyType.mapKeyStorageType()) + val keySort = mapKeySort(mapType.keyType) val valueSort = typeToSort(mapType.valueType) val isRefSet = keySort == addressSort return scope.calcOnState { val destMap = mkSymbolicObjectMap(mapType) if (isRefSet) { - symbolicObjectMapMergeInto(destMap, srcMap, mapType, valueSort) + symbolicObjectMapMergeInto(destMap, source, mapType, valueSort) } else { - symbolicPrimitiveMapMergeInto(destMap, srcMap, mapType, keySort, valueSort, USizeExprKeyInfo()) + symbolicPrimitiveMapCopyIntoEmpty(destMap, source, mapType, keySort, valueSort, USizeExprKeyInfo()) } destMap } } - private fun appendArray(sliceValue: GoValue, appendValue: GoValue): UExpr { - val slice = unboxNamedRef(sliceValue.accept(this).asExpr(ctx.addressSort), sliceValue.type) - val append = unboxNamedRef(appendValue.accept(this).asExpr(ctx.addressSort), appendValue.type) - val sliceType = sliceValue.type.underlying() + 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) + val (equal, less) = compareStringValues(left, right) - val elementSort = if (sliceType == GoBasicTypes.STRING) { - ctx.bv8Sort - } else { - ctx.typeToSort((sliceType as SliceType).elementType) + return 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") } - val zero = ctx.mkSizeExpr(0) + } + + private fun compareStringValues( + left: UHeapRef, + right: UHeapRef, + allowSymbolicIdentity: Boolean = false, + ): Pair { + val sameReference = ctx.mkHeapRefEq(left, right) + if (sameReference.isTrue) return ctx.trueExpr to ctx.falseExpr + + 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 + if (bound == null) { + if (allowSymbolicIdentity) return@calcOnState sameReference to ctx.falseExpr + + 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)) + equal to less + } + } + private fun valuesEqual( + left: UExpr, + right: UExpr, + type: GoType, + guard: UBoolExpr, + representationEquality: Boolean = false, + ): UBoolExpr { + if (guard.isFalse) return ctx.falseExpr + + val underlying = type.underlying() + if (underlying is InterfaceType) { + return interfacesEqual( + left.asExpr(ctx.addressSort), + right.asExpr(ctx.addressSort), + guard, + representationEquality = representationEquality, + ) + } + val lhs = equalityPayload(left, type) + val rhs = equalityPayload(right, type) + + return when (underlying) { + is StructType -> { + var equal: UBoolExpr = ctx.trueExpr + for ((index, fieldType) in underlying.fields.orEmpty().withIndex()) { + val sort = ctx.typeToSort(fieldType) + val fieldLeft = scope.calcOnState { memory.readField(lhs.asExpr(ctx.addressSort), index, sort) } + val fieldRight = scope.calcOnState { memory.readField(rhs.asExpr(ctx.addressSort), index, sort) } + val fieldEqual = valuesEqual( + fieldLeft, + fieldRight, + fieldType, + guard = ctx.mkAnd(guard, equal), + representationEquality = representationEquality, + ) + equal = ctx.mkAnd(equal, fieldEqual) + } + equal + } + is ArrayType -> { + val leftView = scope.calcOnState { arrayView(lhs.asExpr(ctx.addressSort), underlying) } + val rightView = scope.calcOnState { arrayView(rhs.asExpr(ctx.addressSort), underlying) } + val sort = ctx.typeToSort(underlying.elementType) + var equal: UBoolExpr = ctx.trueExpr + for (index in 0 until underlying.len.toInt()) { + val position = ctx.mkSizeExpr(index) + val elementLeft = scope.calcOnState { + memory.readGoArrayIndex( + leftView.backing, + ctx.mkSizeAddExpr(leftView.offset, position), + leftView.storageType, + sort, + ) + } + val elementRight = scope.calcOnState { + memory.readGoArrayIndex( + rightView.backing, + ctx.mkSizeAddExpr(rightView.offset, position), + rightView.storageType, + sort, + ) + } + val elementEqual = valuesEqual( + elementLeft, + elementRight, + underlying.elementType, + guard = ctx.mkAnd(guard, equal), + representationEquality = representationEquality, + ) + equal = ctx.mkAnd(equal, elementEqual) + } + equal + } + GoBasicTypes.STRING -> { + compareStringValues( + lhs.asExpr(ctx.addressSort), + rhs.asExpr(ctx.addressSort), + allowSymbolicIdentity = representationEquality, + ).first + } + is PointerType -> { + referenceEquals(lhs.asExpr(ctx.addressSort), rhs.asExpr(ctx.addressSort)) + } + is BasicType -> { + val equal = GoBinaryOperator.Eql(lhs, rhs).asExpr(ctx.boolSort) + val floating = underlying == GoBasicTypes.FLOAT32 || underlying == GoBasicTypes.FLOAT64 + val comparesStoredFloatRepresentation = representationEquality && floating + if (comparesStoredFloatRepresentation) { + val leftNan = ctx.mkNot(GoBinaryOperator.Eql(lhs, lhs).asExpr(ctx.boolSort)) + val rightNan = ctx.mkNot(GoBinaryOperator.Eql(rhs, rhs).asExpr(ctx.boolSort)) + ctx.mkOr(equal, ctx.mkAnd(leftNan, rightNan)) + } else { + equal + } + } + else -> { + unsupportedExpr("value equality for $type").asExpr(ctx.boolSort) + } + } + } + + private fun equalityPayload(value: UExpr, type: GoType): UExpr { + if (type !is NamedType) return value + + val underlying = type.underlying() + val reference = value.asExpr(ctx.addressSort) + val sort = ctx.typeToSort(underlying) return scope.calcOnState { - val sliceLength = ctx.mkIte( - ctx.mkHeapRefEq(slice, ctx.nullRef), - { zero }, - { memory.readArrayLength(slice, sliceType, ctx.sizeSort) } + ctx.mkIte( + ctx.mkHeapRefEq(reference, ctx.nullRef), + trueBranch = { sampleValue(underlying).asExpr(sort) }, + falseBranch = { unbox(reference, sort).asExpr(sort) }, ) - val appendLength = ctx.mkIte( - ctx.mkHeapRefEq(append, ctx.nullRef), - { zero }, - { memory.readArrayLength(append, sliceType, ctx.sizeSort) } + } + } + + private fun interfacesEqual( + left: UHeapRef, + right: UHeapRef, + guard: UBoolExpr, + representationEquality: Boolean = false, + ): UBoolExpr { + val leftNil = ctx.mkHeapRefEq(left, ctx.nullRef) + val rightNil = ctx.mkHeapRefEq(right, ctx.nullRef) + val bothNonNil = ctx.mkAnd(ctx.mkNot(leftNil), ctx.mkNot(rightNil)) + if (bothNonNil.isFalse) return ctx.mkAnd(leftNil, rightNil) + + var result = ctx.mkAnd(leftNil, rightNil) + val typeSystem = ctx.typeSystem() + val dynamicTypes = program.types.values.distinct().filter(typeSystem::isInstantiable) + for (dynamicType in dynamicTypes) { + val sameType = scope.calcOnState { + ctx.mkAnd( + memory.types.evalIsSubtype(left, dynamicType), + memory.types.evalIsSupertype(left, dynamicType), + memory.types.evalIsSubtype(right, dynamicType), + memory.types.evalIsSupertype(right, dynamicType), + ) + } + val active = ctx.mkAnd(guard, bothNonNil, sameType) + val satisfiable = scope.checkSat(active) != null + if (!satisfiable) continue + + val comparable = isComparable(dynamicType) + if (!comparable) { + scope.fork(ctx.mkNot(active), blockOnFalseState = { panic("comparing uncomparable type $dynamicType") }) + ?: throw GoStepAbort() + continue + } + val sort = ctx.typeToSort(dynamicType) + val lhs = if (dynamicType is NamedType) left else scope.calcOnState { unbox(left, sort) } + val rhs = if (dynamicType is NamedType) right else scope.calcOnState { unbox(right, sort) } + val equal = valuesEqual( + lhs, + rhs, + dynamicType, + guard = active, + representationEquality = representationEquality, ) - checkLength(sliceLength) ?: throw IllegalStateException() - checkLength(appendLength) ?: throw IllegalStateException() + result = ctx.mkOr(result, ctx.mkAnd(bothNonNil, sameType, equal)) + } + return result + } + + private fun ensureMapKeyComparable( + value: UExpr, + type: GoType, + guard: UBoolExpr, + panicOnFailure: Boolean, + ) { + if (guard.isFalse) return + + val underlying = type.underlying() + if (underlying is InterfaceType) { + val reference = value.asExpr(ctx.addressSort) + val nonNil = ctx.mkNot(ctx.mkHeapRefEq(reference, ctx.nullRef)) + val typeSystem = ctx.typeSystem() + val dynamicTypes = program.types.values.distinct().filter { + typeSystem.isInstantiable(it) || !isComparable(it) + } + for (dynamicType in dynamicTypes) { + val dynamic = scope.calcOnState { + ctx.mkAnd( + memory.types.evalIsSubtype(reference, dynamicType), + memory.types.evalIsSupertype(reference, dynamicType), + ) + } + val active = ctx.mkAnd(guard, nonNil, dynamic) + val satisfiable = scope.checkSat(active) != null + if (!satisfiable) continue + + val payload = if (dynamicType is NamedType) { + value + } else { + scope.calcOnState { unbox(reference, ctx.typeToSort(dynamicType)) } + } + ensureMapKeyComparable(payload, dynamicType, active, panicOnFailure) + } + return + } + val comparable = isComparable(type) + if (!comparable) { + val valid = ctx.mkNot(guard) + if (panicOnFailure) { + scope.fork(valid, blockOnFalseState = { panic("hash of unhashable type $type") }) ?: throw GoStepAbort() + } else { + scope.assert(valid) ?: throw GoStepAbort() + } + return + } + val aggregate = underlying is StructType || underlying is ArrayType + if (!aggregate) return + + val payload = equalityPayload(value, type) + when (underlying) { + is StructType -> { + for ((index, fieldType) in underlying.fields.orEmpty().withIndex()) { + val field = scope.calcOnState { + memory.readField(payload.asExpr(ctx.addressSort), index, ctx.typeToSort(fieldType)) + } + ensureMapKeyComparable(field, fieldType, guard, panicOnFailure) + } + } + is ArrayType -> { + val view = scope.calcOnState { arrayView(payload.asExpr(ctx.addressSort), underlying) } + for (index in 0 until underlying.len.toInt()) { + val element = scope.calcOnState { + memory.readGoArrayIndex( + view.backing, + ctx.mkSizeAddExpr(view.offset, ctx.mkSizeExpr(index)), + view.storageType, + ctx.typeToSort(underlying.elementType), + ) + } + ensureMapKeyComparable(element, underlying.elementType, guard, panicOnFailure) + } + } + else -> { + Unit + } + } + } - val length = ctx.mkSizeAddExpr(sliceLength, appendLength) - val out = memory.allocateArray(sliceType, ctx.sizeSort, length) + private fun isComparable(type: GoType): Boolean = when (val underlying = type.underlying()) { + is StructType -> underlying.fields.orEmpty().all(::isComparable) + is ArrayType -> isComparable(underlying.elementType) + is MapType, is SliceType, is SignatureType -> false + else -> true + } - memory.memcpy(slice, out, sliceType, elementSort, zero, zero, sliceLength) - memory.memcpy(append, out, sliceType, elementSort, zero, sliceLength, appendLength) + 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 + } - out + 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 } } @@ -1084,23 +1891,18 @@ class GoExprVisitor( return when (targetType) { is NamedType -> { when (baseType) { - is NamedType -> box(unbox(value.asExpr(ctx.addressSort), ctx.typeToSort(baseType)), targetType) - else -> box(value, targetType) + is NamedType -> { + box(unbox(value.asExpr(ctx.addressSort), ctx.typeToSort(baseType)), targetType) + } + else -> { + box(value, targetType) + } } } is PointerType -> { - ctx.mkIte( - ctx.mkHeapRefEq(value.asExpr(ctx.addressSort), ctx.nullRef), - trueBranch = { ctx.nullRef }, - falseBranch = { - val basePointerType = (baseType as PointerType).baseType - val baseValue = deref(value.asExpr(ctx.addressSort), ctx.typeToSort(basePointerType)) - val targetPointerType = targetType.baseType - val targetValue = changeType(baseValue, basePointerType, targetPointerType) - scope.calcOnState { mkPointer(targetPointerType, targetValue) } - } - ) + val sourceType = (baseType.underlying() as PointerType).baseType + scope.calcOnState { convertPointer(value.asExpr(ctx.addressSort), sourceType, targetType.baseType) } } else -> { @@ -1110,22 +1912,39 @@ class GoExprVisitor( } private fun mockCall(expr: GoCallExpr, func: GoValue): UExpr { - val funcName = when(func) { - is GoParameter -> func.name - is GoFunction -> func.name - else -> "unnamed" + 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 + 0 -> { + NullType() + } + 1 -> { + signature.results.types[0] + } + else -> { + signature.results + } } - val method = GoFunction(signature, emptyList(), funcName, emptyList(), "", emptyList(), emptyList()) + val method = + GoFunction(signature, emptyList(), funcName, emptyList(), packageName = "", emptyList(), emptyList()) val mockSort = ctx.typeToSort(returnType) val mockValue = scope.calcOnState { - val arguments = expr.args.map { it.accept(this@GoExprVisitor) }.asSequence() - memory.mocker.call(method, arguments, mockSort, memory.ownership) + memory.mocker.call( + method, + expr.args.map { it.accept(this@GoExprVisitor) }.asSequence(), + mockSort, + memory.ownership + ) } if (mockSort == ctx.addressSort) { val constraint = scope.calcOnState { diff --git a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt index 2151a1ce32..5a9ad3190a 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoInstVisitor.kt @@ -20,6 +20,7 @@ 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.MapType import org.jacodb.go.api.NullType import org.jacodb.go.api.TupleType import org.usvm.api.collection.ObjectMapCollectionApi.ensureObjectMapSizeCorrect @@ -29,10 +30,14 @@ 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.state.mapKeyPayloadLValue import org.usvm.statistics.ApplicationGraph +import org.usvm.type.needsMapKeyRepresentative import org.usvm.type.underlying class GoInstVisitor( @@ -63,12 +68,18 @@ class GoInstVisitor( 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) + 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 }) - val fields = inst.returnValues.map { it.accept(exprVisitor) }.toTypedArray() - returnValue(mkTuple(type, fields = fields), type) + returnValue( + mkTuple(type, fields = inst.returnValues.map { it.accept(exprVisitor) }.toTypedArray()), + type + ) } } } @@ -99,12 +110,11 @@ class GoInstVisitor( val name = (inst.func.accept(exprVisitor) as KConst).toString() val method = program.findMethod(inst.location, name) - val parameters = inst.args.map { it.accept(exprVisitor) }.toTypedArray() - val call = GoCall(method, applicationGraph.entryPoints(method).first()) - ctx.setMethodInfo(method, parameters) + val parameters = inst.args.map { it.accept(exprVisitor) } scope.doWithState { - data.addDeferredCall(lastEnteredMethod, call) + val call = createCall(method, applicationGraph.entryPoints(method).first(), parameters) + data.addDeferredCall(call) } return next(inst) } @@ -115,15 +125,11 @@ class GoInstVisitor( override fun visitGoStoreInst(inst: GoStoreInst): GoInst { val pointer = inst.lhv.accept(exprVisitor).asExpr(ctx.addressSort) - if (pointer is UNullRef) { - return scope.calcOnState { - panic("null pointer dereference") - next(inst) - } - } + val nonNil = ctx.mkNot(ctx.mkHeapRefEq(pointer, ctx.nullRef)) + scope.fork(nonNil, blockOnFalseState = { panic("null pointer dereference") }) ?: throw GoStepAbort() val rvalue = inst.rhv.accept(exprVisitor) scope.doWithState { - store(pointer, rvalue) + store(pointer, copyValue(rvalue, inst.rhv.type)) } return next(inst) @@ -132,12 +138,30 @@ class GoInstVisitor( override fun visitGoMapUpdateInst(inst: GoMapUpdateInst): GoInst { val map = exprVisitor.unboxNamedRef(inst.map.accept(exprVisitor).asExpr(ctx.addressSort), inst.map.type) val type = inst.map.type.underlying() - val key = exprVisitor.resolveMapKey(inst.key) + val snapshot = exprVisitor.evaluateMapKey(inst.key) val value = inst.value.accept(exprVisitor) - val isRefKey = key.sort == ctx.addressSort + exprVisitor.checkMapWritable(map) ?: throw GoStepAbort() - exprVisitor.checkNotNull(map) ?: throw IllegalStateException() - scope.ensureObjectMapSizeCorrect(map, type) ?: throw IllegalStateException() + val resolvedKey = exprVisitor.resolveMapKeySnapshot(snapshot, inst.key.type) + val key = resolvedKey.key + val isRefKey = key.sort == ctx.addressSort + val mapType = type as MapType + val canMatch = exprVisitor.mapKeyCanMatch(key, mapType.keyType) + + scope.ensureObjectMapSizeCorrect(map, type) ?: throw GoStepAbort() + exprVisitor.ensureMapKeyCanonicality(map, mapType) + val previousMembership = scope.calcOnState { + if (isRefKey) { + memory.read(URefSetEntryLValue(map, key.asExpr(ctx.addressSort), type)) + } else { + memory.read(USetEntryLValue(key.sort, map, key.asExpr(key.sort), type, USizeExprKeyInfo())) + } + } + if (mapType.keyType.needsMapKeyRepresentative()) { + exprVisitor.validateStoredMapKey(map, key.asExpr(ctx.addressSort), mapType, guard = previousMembership) + } + // A NaN insertion needs a fresh entry even when the map came from the input model. + scope.assert(ctx.mkOr(canMatch, ctx.mkNot(previousMembership))) ?: throw GoStepAbort() scope.doWithState { val mapContainsLValue = if (isRefKey) { @@ -152,15 +176,18 @@ class GoInstVisitor( } val currentSize = symbolicObjectMapSize(map, type) - memory.write(mapEntryLValue, value.asExpr(value.sort), ctx.trueExpr) + val keyIsNew = ctx.mkNot(ctx.mkAnd(canMatch, 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) + if (mapType.keyType.needsMapKeyRepresentative()) { + val payload = ctx.mapKeyPayloadLValue(map, key.asExpr(ctx.addressSort), mapType) + memory.write(payload, resolvedKey.snapshot.asExpr(payload.sort), ctx.trueExpr) + } val updatedSize = ctx.mkSizeAddExpr(currentSize, ctx.mkSizeExpr(1)) - memory.write( - UMapLengthLValue(map, type, ctx.sizeSort), - updatedSize, - ctx.mkNot(memory.read(mapContainsLValue)), - ) + memory.write(UMapLengthLValue(map, type, ctx.sizeSort), updatedSize, keyIsNew) } return next(inst) diff --git a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt index a44bcd15ea..5f5d8896f9 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoMachine.kt @@ -31,17 +31,17 @@ import kotlin.math.roundToInt internal typealias USizeSort = UBv32Sort -val logger = object : KLogging() {}.logger +internal val logger = object : KLogging() {}.logger class GoMachine( private val program: GoProgram, override val options: UMachineOptions, private val customOptions: GoMachineOptions, ) : UMachine() { - private val typeSystem = GoTypeSystem(options.typeOperationsTimeout, program.types.values) + private val typeSystem = GoTypeSystem(options.typeOperationsTimeout, program.types.values, program.methodSets) private val applicationGraph = GoApplicationGraph() private val components = GoComponents(typeSystem, options) - private val ctx = GoContext(components) + 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) @@ -62,14 +62,14 @@ class GoMachine( 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 excludedMethods = methodsList.filter { it.hasUnsupportedInstructions() }.map { it.metName } - if (excludedMethods.isNotEmpty()) { - logger.warn { "Skipping methods with unsupported instructions: $excludedMethods" } + val unsupported = methodsList.filter { it.hasUnsupportedInstructions() }.map { it.metName } + if (unsupported.isNotEmpty()) { + throw UnsupportedOperationException("Methods contain unsupported instructions: $unsupported") } - - val methods = methodsList.filter { !it.hasUnsupportedInstructions() } + val methods = methodsList val initialStates = hashMapOf() methods.forEach { initialStates[it] = interpreter.getInitialState(it, targets) @@ -78,11 +78,15 @@ class GoMachine( val coverageStatistics = CoverageStatistics(methods.toSet(), applicationGraph) val callGraphStatistics = when (options.targetSearchDepth) { - 0u -> PlainCallGraphStatistics() - else -> CallGraphStatisticsImpl( - options.targetSearchDepth, - applicationGraph - ) + 0u -> { + PlainCallGraphStatistics() + } + else -> { + CallGraphStatisticsImpl( + options.targetSearchDepth, + applicationGraph + ) + } } val pathSelector = createPathSelector( @@ -96,12 +100,18 @@ class GoMachine( ) val statesCollector = when (options.stateCollectionStrategy) { - StateCollectionStrategy.COVERED_NEW -> CoveredNewStatesCollector(coverageStatistics) { - it.methodResult is GoMethodResult.Panic + StateCollectionStrategy.COVERED_NEW -> { + CoveredNewStatesCollector(coverageStatistics) { + it.methodResult is GoMethodResult.Panic + } } - StateCollectionStrategy.REACHED_TARGET -> TargetsReachedStatesCollector() - StateCollectionStrategy.ALL -> AllStatesCollector() + StateCollectionStrategy.REACHED_TARGET -> { + TargetsReachedStatesCollector() + } + StateCollectionStrategy.ALL -> { + AllStatesCollector() + } } val stepsStatistics = StepsStatistics() val stopStrategy = createStopStrategy( @@ -143,6 +153,7 @@ class GoMachine( ) } + interpreter.unsupportedOperations.clear() run( interpreter, pathSelector, @@ -151,29 +162,28 @@ class GoMachine( stopStrategy = stopStrategy, ) - val totalCoverage = coverageStatistics.getTotalCoverage().roundToInt() - logger.info { "Total coverage: $totalCoverage%" } + interpreter.unsupportedOperations.firstOrNull()?.let { throw it } + + logger.debug("Total coverage: ${coverageStatistics.getTotalCoverage().roundToInt()}%") for (method in coverageStatistics.coverageZone) { - val methodCoverage = coverageStatistics.getMethodCoverage(method).roundToInt() - logger.info { "Method ${method.metName} coverage: $methodCoverage%" } + logger.debug( + "Method ${method.metName} coverage: ${coverageStatistics.getMethodCoverage(method).roundToInt()}%" + ) } - val analyzedMethods = methods.map { it.metName } - val hasUnexpectedUncoveredMethods = totalCoverage < FULL_COVERAGE_PERCENT && - !customOptions.uncoveredMethods.containsAll(analyzedMethods) - if (hasUnexpectedUncoveredMethods) { - if (customOptions.failOnNotFullCoverage) { - throw IllegalStateException("coverage not 100%") - } else { - logger.warn { "Analysis of methods $analyzedMethods led to coverage < 100%" } - } + 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() } } - -private const val FULL_COVERAGE_PERCENT = 100 diff --git a/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt b/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt index 8a6291e2f9..0c447f03b1 100644 --- a/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt +++ b/usvm-go/src/main/kotlin/org/usvm/GoTarget.kt @@ -3,6 +3,6 @@ package org.usvm import org.jacodb.go.api.GoInst import org.usvm.targets.UTarget -abstract class GoTarget( +open class GoTarget( location: GoInst, ) : UTarget(location) diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt index 302d41531e..535d396120 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoInterpreter.kt @@ -22,6 +22,7 @@ import org.usvm.forkblacklists.UForkBlackList import org.usvm.solver.USatResult import org.usvm.state.GoFlowStatus import org.usvm.state.GoState +import org.usvm.state.advanceArrayValueCopy import org.usvm.statistics.ApplicationGraph import org.usvm.targets.UTargetsSet @@ -33,14 +34,13 @@ class GoInterpreter( private val applicationGraph: ApplicationGraph, private var forkBlackList: UForkBlackList = UForkBlackList.createDefault(), ) : UInterpreter() { + internal val unsupportedOperations: MutableList = mutableListOf() + + @Suppress("NestedBlockDepth") // Initializes pointer chains for package globals. fun getInitialState(method: GoMethod, targets: List = emptyList()): GoState = with(ctx) { val initOwnership = MutabilityOwnership() val state = GoState(ctx, initOwnership, method, targets = UTargetsSet.from(targets)) - val solver = solver() - val model = (solver.check(state.pathConstraints) as USatResult).model - state.models = listOf(model) - for (global in program.globals) { var type = global.type var ref = mkConcreteHeapRef(NULL_ADDRESS) @@ -49,7 +49,7 @@ class GoInterpreter( type = type.baseType depth++ } - for (i in 0 until depth) { + repeat(depth) { ref = if (ref.address != NULL_ADDRESS) { state.mkPointer(type, ref) } else { @@ -61,29 +61,42 @@ class GoInterpreter( } val entrypoint = method.blocks[0].instructions[0] - setMethodInfo(method) state.addCall(GoCall(method, entrypoint)) var previousEntrypoint = entrypoint - for (m in program.findInitMethods(method.packageName)+program.findOsInitMethods()) { - setMethodInfo(m) + 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) - val nextInst = next(state, inst, instVisitor) - if (nextInst !is GoNullInst) { - state.newInst(nextInst) + 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() } @@ -91,9 +104,11 @@ class GoInterpreter( 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.NORMAL -> { + inst.accept(instVisitor) + } GoFlowStatus.DEFER -> { - val deferred = state.data.getDeferredCalls(method) + val deferred = state.data.getDeferredCalls() if (deferred.isEmpty()) { state.data.flowStack.removeLast() return next(state, inst, instVisitor) @@ -108,10 +123,12 @@ class GoInterpreter( state.data.flowStack.removeLast() val function = method as GoFunction function.setRecover() - return function.recover!!.instructions.first() + return checkNotNull( + function.recover + ) { "Recovered function has no recovery block" }.instructions.first() } - if (state.data.getDeferredCalls(method).isEmpty()) { + if (state.data.getDeferredCalls().isEmpty()) { state.handlePanic() return GoNullInst(method) } diff --git a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoMapEntries.kt b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoMapEntries.kt new file mode 100644 index 0000000000..eeeea5b5be --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoMapEntries.kt @@ -0,0 +1,19 @@ +package org.usvm.interpreter + +/** A lossless witness when JVM Map equality would collapse distinct Go map entries. */ +data class GoMapEntries(val entries: List) { + val size: Int get() = entries.size +} + +/** Identity is stable across the before/after witnesses of one symbolic execution. */ +data class GoMapEntry(val key: Any?, val value: Any?, val identity: String) + +internal fun needsGoMapEntryIdentity(key: Any?): Boolean = when (key) { + is Double -> key.isNaN() || key == 0.0 && key.toRawBits() < 0 + is Float -> key.isNaN() || key == 0.0f && key.toRawBits() < 0 + is GoPointer -> true + is GoInterfaceValue -> needsGoMapEntryIdentity(key.value) + is List<*> -> key.any(::needsGoMapEntryIdentity) + is Map<*, *> -> key.values.any(::needsGoMapEntryIdentity) + else -> false +} 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 index 25d406da0c..47485767a4 100644 --- a/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/interpreter/GoTestInterpreter.kt @@ -6,6 +6,7 @@ import io.ksmt.expr.KBitVec64Value import io.ksmt.expr.KBitVec8Value import io.ksmt.expr.KFp32Value import io.ksmt.expr.KFp64Value +import io.ksmt.expr.KIteExpr import io.ksmt.sort.KBoolSort import io.ksmt.sort.KBv16Sort import io.ksmt.sort.KBv32Sort @@ -23,19 +24,22 @@ import org.jacodb.go.api.MapType import org.jacodb.go.api.NamedType import org.jacodb.go.api.NullType import org.jacodb.go.api.PointerType +import org.jacodb.go.api.SignatureType import org.jacodb.go.api.SliceType import org.jacodb.go.api.StructType import org.jacodb.go.api.TupleType import org.usvm.GoContext -import org.usvm.NULL_ADDRESS import org.usvm.UAddressSort +import org.usvm.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.readArrayIndex -import org.usvm.api.readArrayLength +import org.usvm.api.makeSymbolicRefUntyped import org.usvm.api.readField import org.usvm.api.typeStreamOf import org.usvm.collection.map.length.UMapLengthLValue @@ -45,21 +49,33 @@ import org.usvm.collection.set.primitive.USetEntryLValue import org.usvm.collection.set.primitive.setEntries import org.usvm.collection.set.ref.URefSetEntryLValue import org.usvm.collection.set.ref.refSetEntries -import org.usvm.interpreter.GoInterpreter.Companion.logger +import org.usvm.collections.immutable.internal.MutabilityOwnership +import org.usvm.constraints.UPathConstraints import org.usvm.isTrue import org.usvm.memory.ULValue -import org.usvm.memory.URegisterStackLValue +import org.usvm.memory.UMemory import org.usvm.memory.UWritableMemory +import org.usvm.memory.arrayView import org.usvm.memory.key.USizeExprKeyInfo +import org.usvm.memory.readGoArrayIndex +import org.usvm.mkSizeAddExpr import org.usvm.mkSizeExpr +import org.usvm.mkSizeGeExpr +import org.usvm.mkSizeLeExpr import org.usvm.model.UModelBase +import org.usvm.operator.GoBinaryOperator 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.state.mapKeyCanonicalityConstraint +import org.usvm.state.mapKeyPayloadLValue +import org.usvm.state.mapKeySnapshotLValue import org.usvm.type.GoBasicTypes import org.usvm.type.GoVoidSort import org.usvm.type.mapKeyStorageType +import org.usvm.type.needsMapKeyRepresentative import org.usvm.type.underlying import org.usvm.types.first import java.nio.ByteBuffer @@ -70,27 +86,72 @@ import kotlin.random.nextULong class GoTestInterpreter( private val ctx: GoContext, ) { - fun resolve(state: GoState, method: GoMethod): ProgramExecutionResult = with(ctx) { - val model = state.models.first() + fun resolve(state: GoState, method: GoMethod): ProgramExecutionResult { + var model = state.models.first() + val boundedConstraints by lazy { state.clone().pathConstraints } + val bounds = mutableSetOf() + + repeat(MAX_MODEL_REFINEMENTS) { + try { + return resolveWithModel(state, method, model) + } catch (limit: MaterializationBoundException) { + if (!bounds.add(limit.bound)) { + throw UnsupportedOperationException(limit.failureMessage, limit) + } + model = refineModel(boundedConstraints, limit) + } + } + throw UnsupportedOperationException("Go model size refinement limit exceeded") + } - val inputScope = MemoryScope(ctx, state, model, model) + private fun refineModel( + constraints: UPathConstraints, + boundFailure: MaterializationBoundException, + ): UModelBase { + constraints += boundFailure.bound + val result = ctx.solver().check(constraints) + return (result as? USatResult)?.model ?: throw UnsupportedOperationException( + "${boundFailure.failureMessage}: $result", + boundFailure, + ) + } + + private fun resolveWithModel( + state: GoState, + method: GoMethod, + model: UModelBase, + ): ProgramExecutionResult = with(ctx) { + // Keep input reads symbolic so size refinement can re-solve them without using the mutated heap. + val inputMemory = UMemory(ctx, MutabilityOwnership(), state.pathConstraints.typeConstraints) + val inputScope = MemoryScope(ctx, state, model, inputMemory) val outputScope = MemoryScope(ctx, state, model, state.memory) val inputValues = List(method.parameters.size) { idx -> val type = method.parameters[idx].type as GoType val sort = typeToSort(type) - val expr = model.read(URegisterStackLValue(sort, idx)) + val expr = mkRegisterReading(idx, sort) inputScope.convertExpr(expr, type) } val inputModel = InputModel(inputValues) + 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)) + UnsuccessfulExecutionResult(inputModel, outputScope.convertExpr(panic.value, panic.type), argumentsAfter) } else { val result = state.methodResult as GoMethodResult.Success val expr = result.let { outputScope.convertExpr(it.value, it.type) } - val outputModel = OutputModel(expr) + val outputModel = OutputModel(expr, argumentsAfter) SuccessfulExecutionResult(inputModel, outputModel) } @@ -102,43 +163,91 @@ class GoTestInterpreter( private val model: UModelBase, private val memory: UWritableMemory, ) { - fun convertExpr(expr: UExpr, baseType: GoType): Any = when (expr.sort) { - is GoVoidSort -> "" - is KBoolSort -> resolveBool(expr) - is KBv8Sort -> resolveBv8(expr) - is KBv16Sort -> resolveBv16(expr) - is KBv32Sort -> resolveBv32(expr).let { if (baseType == GoBasicTypes.RUNE) Char(it) else it } - is KBv64Sort -> resolveBv64(expr) - is KFp32Sort -> resolveFp32(expr) - is KFp64Sort -> resolveFp64(expr) - is UAddressSort -> when (expr.sort) { - ctx.addressSort -> { - val h = expr.asExpr(ctx.addressSort) - val named = baseType is NamedType - val prefix = if (named) (baseType as NamedType).name + ": " else "" - prefix + when (val type = baseType.underlying()) { - GoBasicTypes.STRING -> if (named) resolveBoxed(h, type) else resolveString(h, type) - is BasicType -> resolveBoxed(h, type) - is ArrayType -> { - if (named) resolveBoxed(h, type) else resolveArray(h, type, type.len, type.elementType) - } - is SliceType -> if (named) resolveBoxed(h, type) else resolveSlice(h, type, type.elementType) - is MapType -> { - if (named) resolveBoxed(h, type) else resolveMap(h, type, type.keyType, type.valueType) - } - is TupleType -> resolveTuple(h, type) - is StructType -> if (named) resolveBoxed(h, type) else resolveStruct(h, type) - is InterfaceType -> resolveInterface(h) - is NullType -> null - is PointerType -> resolvePointer(h, type.baseType) - else -> Any() - } + 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() + else -> { + Any() } + } - 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 @@ -157,18 +266,39 @@ class GoTestInterpreter( fun resolveSize(expr: UExpr) = (model.eval(expr) as KBitVec32Value).numberValue + private fun resolveLength(expr: UExpr): Int { + val length = resolveSize(expr) + if (length !in 0..MAX_ARRAY_LENGTH) { + val bound = ctx.mkAnd( + ctx.mkSizeGeExpr(expr, ctx.mkSizeExpr(0)), + ctx.mkSizeLeExpr(expr, ctx.mkSizeExpr(MAX_ARRAY_LENGTH)) + ) + throw MaterializationBoundException( + bound, + failureMessage = "Cannot materialize Go model within size limit", + ) + } + return length + } + fun resolveString(string: UHeapRef, arrayType: GoType): String = with(ctx) { - if (string == mkConcreteHeapRef(NULL_ADDRESS) || string == nullRef) { + if (model.eval(string) == model.nullRef) { return "" } - val lengthUExpr = memory.readArrayLength(string, arrayType, sizeSort) - val length = clipArrayLength(resolveSize(lengthUExpr)) + 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 (array == mkConcreteHeapRef(NULL_ADDRESS) || array == nullRef) { + if (model.eval(array) == model.nullRef) { return null } - val length = clipArrayLength(len.toInt()) + val view = state.arrayView(array, arrayType, sourceMemory = memory) + if (len !in 0..MAX_ARRAY_LENGTH.toLong()) { + throw UnsupportedOperationException("Cannot materialize Go array of length $len") + } + val length = len.toInt() val sort = typeToSort(elementType) return List(length) { idx -> - val element = memory.readArrayIndex(array, mkSizeExpr(idx), arrayType, sort) + val element = memory.readGoArrayIndex( + view.backing, + mkBvAddExpr(view.offset, mkSizeExpr(idx)), + view.storageType, + sort + ) convertExpr(element, elementType) } } fun resolveSlice(slice: UHeapRef, sliceType: GoType, elementType: GoType): List? = with(ctx) { - if (slice == mkConcreteHeapRef(NULL_ADDRESS) || slice == nullRef) { + if (model.eval(slice) == model.nullRef) { return null } - val lengthUExpr = memory.readArrayLength(slice, sliceType, sizeSort) - val length = clipArrayLength(resolveSize(lengthUExpr)) + 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 element = memory.readArrayIndex(slice, mkSizeExpr(idx), sliceType, sort) + val offset = view.offset + val index = ctx.mkBvAddExpr(offset, mkSizeExpr(idx)) + val element = memory.readGoArrayIndex( + view.backing, + index, + view.storageType, + sort + ) convertExpr(element, elementType) } } - fun resolveMap( - map: UHeapRef, - mapType: GoType, - keyType: GoType, - valueType: GoType, - ): Map? = with(ctx) { - if (map == mkConcreteHeapRef(NULL_ADDRESS) || map == nullRef) { + @Suppress("NestedBlockDepth") // Resolves both primitive and reference map regions. + fun resolveMap(map: UHeapRef, mapType: GoType, keyType: GoType, valueType: GoType): Any? = with( + ctx + ) { + if (model.eval(map) == model.nullRef) { return null } val keyStorageType = keyType.mapKeyStorageType() - val keySort = typeToSort(keyStorageType) + val keySort = mapKeySort(keyType) val valueSort = typeToSort(valueType) val isRefSet = keySort == addressSort - val addToMap: (MutableMap, Set>) -> Unit = { m, s -> - m.putAll(s.associate { entry -> - val key = when (entry) { - is URefSetEntryLValue<*> -> entry.setElement - is USetEntryLValue<*, *, *> -> entry.setElement - else -> throw IllegalStateException() - } - - val lvalue = if (isRefSet) { - URefMapEntryLValue(valueSort, map, key.asExpr(addressSort), mapType) - } else { - UMapEntryLValue(keySort, valueSort, map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) - } - val value = memory.read(lvalue) - convertExpr(key, keyStorageType) to convertExpr(value, valueType) - }) - } - val getEntries: (UHeapRef) -> Set> = { - if (isRefSet) { - memory.refSetEntries(it, mapType) - } else { - memory.setEntries(it, mapType, keySort, USizeExprKeyInfo()) - }.entries + val usesRepresentatives = keyType.needsMapKeyRepresentative() + if (usesRepresentatives) { + val canonicality = state.mapKeyCanonicalityConstraint(map, mapType as MapType, sourceMemory = memory) + if (!resolveBool(canonicality)) throw MaterializationBoundException(canonicality) } + val seenRepresentatives = mutableSetOf() + val seenValueKeys = mutableSetOf() + val losslessEntries = mutableListOf() + val observedKeys = state.data.mapKeyRepresentatives[keyType].orEmpty() + .filter { entry -> resolveBool(entry.guard) } + .flatMap { entry -> listOf(entry.reference, entry.canonicalReference) } + .toMutableSet() + var needsLosslessWitness = false + val addToMap: ( + MutableMap, + Set>, + UWritableMemory, + UHeapRef, + ) -> Unit = { destination, entries, entryMemory, entryMap -> + val entryScope = MemoryScope(ctx, state, model, entryMemory) + destination.putAll( + entries.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(entryMap, key.asExpr(addressSort), mapType) + } else { + USetEntryLValue(keySort, entryMap, key.asExpr(keySort), mapType, USizeExprKeyInfo()) + } + if (usesRepresentatives) observedKeys += key.asExpr(addressSort) + if (!resolveBool(entryMemory.read(membership))) return@mapNotNull null + val decodedKey = entryScope.decodeMapKey(entryMap, key, mapType, keyType) + if (usesRepresentatives) { + val concrete = model.eval(key.asExpr(addressSort)) + if (!seenRepresentatives.add(concrete)) return@mapNotNull null + needsLosslessWitness = needsGoMapEntryIdentity(decodedKey) || + !seenValueKeys.add(decodedKey) || needsLosslessWitness + } - val length = clipArrayLength(resolveSize(memory.read(UMapLengthLValue(map, mapType, sizeSort)))) + val lvalue = mapValueLValue(entryMap, key, mapType, keySort, valueSort) + val value = entryScope.convertExpr(entryMemory.read(lvalue), valueType) + if (usesRepresentatives) { + losslessEntries += GoMapEntry(decodedKey, value, identity = model.eval(key).toString()) + } + decodedKey to value + }.toMap() + ) + } + val lengthExpression = memory.read(UMapLengthLValue(map, mapType, sizeSort)) + val length = resolveLength(lengthExpression) val result = mutableMapOf() - getEntries(map).also { addToMap(result, it) } - getEntries(model.eval(map)).also { addToMap(result, it) } + getMapEntries(memory, map, mapType, keySort).also { addToMap(result, it, memory, map) } + val concreteMap = model.eval(map) + getMapEntries(model, concreteMap, mapType, keySort).also { addToMap(result, it, model, concreteMap) } + + if (usesRepresentatives) { + ensureMapEntriesMaterialized( + map, + mapType, + keyType, + observedKeys, + lengthExpression, + length = length, + observedCount = losslessEntries.size, + ) + return if (needsLosslessWitness) GoMapEntries(losslessEntries.toList()) else result + } if (length > result.size) { val diff = length - result.size @@ -268,8 +461,218 @@ class GoTestInterpreter( return result } + private fun mapValueLValue( + map: UHeapRef, + key: UExpr, + mapType: GoType, + keySort: USort, + valueSort: USort, + ): ULValue<*, USort> { + val isReferenceKey = keySort == ctx.addressSort + return if (isReferenceKey) { + URefMapEntryLValue(valueSort, map, key.asExpr(ctx.addressSort), mapType) + } else { + UMapEntryLValue(keySort, valueSort, map, key.asExpr(keySort), mapType, USizeExprKeyInfo()) + } + } + + private fun getMapEntries( + source: UWritableMemory, + map: UHeapRef, + mapType: GoType, + keySort: USort, + ): Set> { + val isReferenceKey = keySort == ctx.addressSort + return if (isReferenceKey) { + source.refSetEntries(map, mapType).entries + } else { + source.setEntries(map, mapType, keySort, USizeExprKeyInfo()).entries + } + } + + private fun decodeMapKey(map: UHeapRef, key: UExpr, mapType: GoType, keyType: GoType): Any? { + val usesRepresentatives = keyType.needsMapKeyRepresentative() + if (usesRepresentatives) { + val stored = memory.read(ctx.mapKeyPayloadLValue(map, key.asExpr(ctx.addressSort), mapType as MapType)) + return convertStoredMapKey(stored, keyType) + } + + return convertExpr(key, keyType.mapKeyStorageType()) + } + + private fun ensureMapEntriesMaterialized( + map: UHeapRef, + mapType: GoType, + keyType: GoType, + observedKeys: Set, + lengthExpression: UExpr, + length: Int, + observedCount: Int, + ): Unit = with(ctx) { + if (length == observedCount) return + + // Unobserved entries need a solver-backed witness, never invented keys or values. + val canGenerateKeys = keyType.underlying() in setOf( + GoBasicTypes.FLOAT32, + GoBasicTypes.FLOAT64, + GoBasicTypes.STRING, + ) + val canGenerateHiddenEntries = length > observedCount && canGenerateKeys + val bound = if (canGenerateHiddenEntries) { + constrainHiddenMapEntries( + map, + mapType, + keyType, + observedKeys, + lengthExpression, + length = length, + hiddenCount = minOf(length - observedCount, MAX_NEW_MAP_KEYS_PER_REFINEMENT), + ) + } else { + mkEq(lengthExpression, mkSizeExpr(observedCount)) + } + throw MaterializationBoundException(bound) + } + + private fun convertStoredMapKey(value: UExpr, type: GoType): Any? { + val underlying = type.underlying() + val referenceZero = value.sort == ctx.addressSort && + model.eval(value.asExpr(ctx.addressSort)) == model.nullRef + if (referenceZero) return zeroStoredMapKey(underlying) + + return convertExpr(value, type) + } + + private fun zeroStoredMapKey(type: GoType): Any? = when (val underlying = type.underlying()) { + is ArrayType -> { + val bounded = underlying.len in 0..MAX_ARRAY_LENGTH.toLong() + if (!bounded) { + throw UnsupportedOperationException( + "Cannot materialize zero Go array key of length ${underlying.len}", + ) + } + + List(underlying.len.toInt()) { zeroStoredMapKey(underlying.elementType) } + } + is StructType -> { + underlying.fields.orEmpty().mapIndexed { index, field -> + "field$index" to zeroStoredMapKey(field) + }.toMap() + } + else -> { + convertExpr(ctx.typeToSort(underlying).sampleUValue(), underlying) + } + } + + private fun mapKeyPayload( + key: UHeapRef, + type: GoType, + map: UHeapRef, + mapType: MapType, + ): Pair, UWritableMemory> = with(ctx) { + val known = state.data.mapKeyRepresentatives[type].orEmpty().firstOrNull { + model.eval(it.reference) == model.eval(key) && resolveBool(it.guard) + } + val source = if (known == null) model else state.memory + val snapshot = known?.snapshot ?: source.read( + ctx.mapKeyPayloadLValue(model.eval(map), model.eval(key), mapType), + ) + val underlying = type.underlying() + val sort = typeToSort(underlying) + val payload = if (type is NamedType) { + mkIte( + mkHeapRefEq(snapshot.asExpr(addressSort), nullRef), + sort.sampleUValue(), + source.readField(snapshot.asExpr(addressSort), field = 0, sort = sort), + ) + } else { + snapshot + } + payload to source + } + + private fun constrainHiddenMapEntries( + map: UHeapRef, + mapType: GoType, + keyType: GoType, + observed: Set, + lengthExpression: UExpr, + length: Int, + hiddenCount: Int, + ): UBoolExpr = with(ctx) { + val underlying = keyType.underlying() + val keys = observed.toMutableList() + val payloads = keys.map { mapKeyPayload(it, keyType, map, mapType as MapType).first }.toMutableList() + val stringLengthExpressions = if (underlying == GoBasicTypes.STRING) { + keys.map { key -> + val (payload, source) = mapKeyPayload(key, keyType, map, mapType as MapType) + state.arrayView(payload.asExpr(addressSort), underlying, sourceMemory = source).length + }.toMutableList() + } else { + mutableListOf() + } + val usedStringLengths = stringLengthExpressions.map(::resolveLength).toMutableSet() + var constraints = mkEq(lengthExpression, mkSizeExpr(length)) + repeat(hiddenCount) { + val fresh = state.makeSymbolicRefUntyped() + val snapshot = memory.read(ctx.mapKeySnapshotLValue(fresh, keyType)) + val sort = typeToSort(underlying) + val payload = if (keyType is NamedType) { + mkIte( + mkHeapRefEq(snapshot.asExpr(addressSort), nullRef), + sort.sampleUValue(), + memory.readField(snapshot.asExpr(addressSort), field = 0, sort = sort), + ) + } else { + snapshot + } + constraints = mkAnd( + constraints, + memory.read(URefSetEntryLValue(map, fresh, mapType)), + mkEq(memory.read(ctx.mapKeyPayloadLValue(map, fresh, mapType as MapType)), snapshot), + mkNot(mkHeapRefEq(fresh, nullRef)), + ) + for (key in keys) constraints = mkAnd(constraints, mkNot(mkHeapRefEq(fresh, key))) + if (underlying == GoBasicTypes.STRING) { + val nextLength = (0..MAX_ARRAY_LENGTH).firstOrNull { candidateLength -> + candidateLength !in usedStringLengths + } ?: throw UnsupportedOperationException("No bounded distinct string key for Go map witness") + usedStringLengths += nextLength + val view = state.arrayView(payload.asExpr(addressSort), underlying, sourceMemory = memory) + constraints = mkAnd(constraints, mkEq(view.length, mkSizeExpr(nextLength))) + for (other in stringLengthExpressions) { + constraints = mkAnd(constraints, mkNot(mkEq(view.length, other))) + } + stringLengthExpressions += view.length + if (nextLength > 0) { + constraints = mkAnd(constraints, mkNot(mkHeapRefEq(payload.asExpr(addressSort), nullRef))) + } + for (index in 0 until nextLength) { + val byte = memory.readGoArrayIndex( + view.backing, + mkSizeAddExpr(view.offset, mkSizeExpr(index)), + view.storageType, + bv8Sort, + ) + val asciiByte = mkBvUnsignedLessOrEqualExpr(byte, mkBv(MAX_ASCII_BYTE, bv8Sort)) + constraints = mkAnd(constraints, asciiByte) + } + } else { + val reflexive = GoBinaryOperator.Eql(payload, payload).asExpr(boolSort) + constraints = mkAnd(constraints, reflexive) + for (other in payloads) { + val equal = GoBinaryOperator.Eql(payload, other).asExpr(boolSort) + constraints = mkAnd(constraints, mkNot(equal)) + } + } + keys += fresh + payloads += payload + } + constraints + } + fun resolveTuple(tuple: UHeapRef, tupleType: TupleType): List? = with(ctx) { - if (tuple == mkConcreteHeapRef(NULL_ADDRESS) || tuple == nullRef) { + if (model.eval(tuple) == model.nullRef) { return null } @@ -280,39 +683,66 @@ class GoTestInterpreter( } fun resolveStruct(struct: UHeapRef, structType: StructType): Map? = with(ctx) { - if (struct == mkConcreteHeapRef(NULL_ADDRESS) || struct == nullRef) { + if (model.eval(struct) == model.nullRef) { return null } return structType.fields?.mapIndexed { idx, type -> idx to type }?.associate { - val (index, fieldType) = it - val fieldValue = memory.readField(struct, index, typeToSort(fieldType)) - Pair("field$index", convertExpr(fieldValue, fieldType)) + Pair( + "field${it.first}", + convertExpr(memory.readField(struct, it.first, typeToSort(it.second)), it.second) + ) } } fun resolveInterface(iface: UHeapRef): Any? = with(ctx) { - if (iface == mkConcreteHeapRef(NULL_ADDRESS) || iface == nullRef) { + if (model.eval(iface) == model.nullRef) { return null } - val type = memory.typeStreamOf(iface).first() + val type = if (org.usvm.isAllocatedConcreteHeapRef(iface)) { + state.memory.typeStreamOf(iface).first() + } else { + model.typeStreamOf(model.eval(iface)).first() + } val index = 0 - return convertExpr(memory.readField(iface, index, typeToSort(type)), type) + val value = if (type is NamedType) { + convertExpr(iface, type) + } else { + convertExpr(memory.readField(iface, index, typeToSort(type)), type) + } + return GoInterfaceValue(type, value) } fun resolvePointer(pointer: UHeapRef, baseType: GoType): Any? = with(ctx) { - if (pointer == mkConcreteHeapRef(NULL_ADDRESS) || pointer == nullRef) { + if (pointer is KIteExpr) { + val branch = if (resolveBool(pointer.condition)) pointer.trueBranch else pointer.falseBranch + return resolvePointer(branch, baseType) + } + state.data.pointerConversions[pointer]?.let { conversion -> + return resolvePointer(conversion.source, conversion.sourceType) + } + + if (model.eval(pointer) == model.nullRef) { return null } val index = 0 - val expr = memory.readField(pointer, index, ctx.typeToSort(baseType)) - return "&" + convertExpr(expr, baseType) + 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 (value == mkConcreteHeapRef(NULL_ADDRESS)) { + if (model.eval(value) == model.nullRef) { return null } @@ -321,30 +751,23 @@ class GoTestInterpreter( } } - companion object { - fun clipArrayLength(length: Int): Int = - when { - length in 0..MAX_ARRAY_LENGTH -> length - - length > MAX_ARRAY_LENGTH -> { - logger.warn { "Array length exceeds $MAX_ARRAY_LENGTH: $length" } - MAX_ARRAY_LENGTH - } - - else -> { - logger.warn { "Negative array length: $length" } - 0 - } - } - - private const val MAX_ARRAY_LENGTH = 10_000 + private companion object { + const val MAX_ARRAY_LENGTH = 10_000 + const val MAX_MODEL_REFINEMENTS = 32 + const val MAX_NEW_MAP_KEYS_PER_REFINEMENT = 64 + const val MAX_ASCII_BYTE = 127 } + + private class MaterializationBoundException( + val bound: UBoolExpr, + val failureMessage: String = "Cannot materialize Go model under witness constraints", + ) : RuntimeException() } sealed interface ProgramExecutionResult class InputModel( - private val arguments: List, + val arguments: List, ) { override fun toString(): String { return buildString { @@ -356,7 +779,8 @@ class InputModel( } class OutputModel( - private val returnExpr: Any?, + val returnExpr: Any?, + val argumentsAfter: List, ) { override fun toString(): String { return buildString { @@ -368,8 +792,8 @@ class OutputModel( } class SuccessfulExecutionResult( - private val inputModel: InputModel, - private val outputModel: OutputModel, + val inputModel: InputModel, + val outputModel: OutputModel, ) : ProgramExecutionResult { override fun toString(): String { return buildString { @@ -385,8 +809,9 @@ class SuccessfulExecutionResult( } class UnsuccessfulExecutionResult( - private val inputModel: InputModel, - private val result: Any?, + val inputModel: InputModel, + val panicValue: Any?, + val argumentsAfter: List, ) : ProgramExecutionResult { override fun toString(): String { return buildString { @@ -395,7 +820,7 @@ class UnsuccessfulExecutionResult( appendLine("----------------------------------------------------------------") appendLine(inputModel.toString()) appendLine("----------------------------------------------------------------") - appendLine(result) + appendLine(panicValue) appendLine("================================================================") } } @@ -449,7 +874,9 @@ class RNG( { random.nextDouble() } } - else -> throw IllegalStateException() + else -> { + error("Inconsistent Go interpreter state") + } } fun generateUniqueMapKey(map: Map): Any? { @@ -463,3 +890,14 @@ class RNG( 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 index 279e57f377..5f7465f4fa 100644 --- a/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayAllocation.kt +++ b/usvm-go/src/main/kotlin/org/usvm/memory/GoArrayAllocation.kt @@ -13,7 +13,7 @@ internal fun UWritableMemory.allocateGoArray( count: UExpr, ): UConcreteHeapRef { val reference = allocConcrete(type) - initializeArrayLength(reference, type, sizeSort, count) + initializeArrayLength(reference, type.arrayStorageType(), sizeSort, count) return reference } @@ -24,6 +24,6 @@ internal fun UWritableMemory.allocateGo contents: Sequence>, ): UConcreteHeapRef { val reference = allocConcrete(type) - initializeArray(reference, type, sort, sizeSort, contents) + 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 index dd013d1d55..46cc9429e9 100644 --- a/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt +++ b/usvm-go/src/main/kotlin/org/usvm/model/Converter.kt @@ -103,10 +103,15 @@ import org.usvm.GoPackage import org.usvm.type.GoBasicTypes import org.usvm.type.GoTypes -object Converter { +@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() - fun unpack(pkg: Package): GoPackage { + 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( @@ -130,7 +135,16 @@ object Converter { val globals = pkg.members.filterIsInstance().map { global -> GoGlobal(global.index, global.name, getType(global.goType)) } - return GoPackage(pkg.name, methods, globals, typesMap) + 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 { @@ -141,26 +155,60 @@ object Converter { 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.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() + 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() + 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() + 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.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() + GoFieldExpr( + loc, + getType(inst.goType), + unpack(inst.struct), + inst.field, + inst.register + ).toAssignInst() } is Instruction.FieldAddr -> { GoFieldAddrExpr( @@ -168,78 +216,146 @@ object Converter { getType(inst.goType), unpack(inst.struct), inst.field, - inst.register, + inst.register ).toAssignInst() } - is Instruction.Go -> unsupportedInstruction(function) + is Instruction.Go -> { + unsupportedInstruction(function) + } is Instruction.If -> { GoIfInst( loc, unpack(inst.condition) as GoConditionExpr, GoInstRef(inst.trueBranch), - GoInstRef(inst.falseBranch), + 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.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() + 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() + 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.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() + GoNextExpr( + loc, + getType(inst.goType), + unpack(inst.iter), + inst.register + ).toAssignInst() + } + is Instruction.Panic -> { + GoPanicInst(loc, unpack(inst.value)) } - is Instruction.Panic -> GoPanicInst(loc, unpack(inst.value)) is Instruction.Phi -> { - GoPhiExpr(loc, getType(inst.goType), inst.edges.map(this::unpack), inst.register).toAssignInst() + 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) + 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 constructors = mapOf>( - "&&" to ::GoAndExpr, - "&^" to ::GoAndNotExpr, - "||" to ::GoOrExpr, - "^" to ::GoXorExpr, - "==" to ::GoEqlExpr, - "!=" to ::GoNeqExpr, - "<" to ::GoLssExpr, - "<=" to ::GoLeqExpr, - ">" to ::GoGtrExpr, - ">=" to ::GoGeqExpr, - "+" to ::GoAddExpr, - "-" to ::GoSubExpr, - "*" to ::GoMulExpr, - "/" to ::GoDivExpr, - "%" to ::GoModExpr, - "<<" to ::GoShlExpr, - ">>" to ::GoShrExpr, - ) - - if (constructors.containsKey(binOp.op)) { - return constructors[binOp.op]!!( + 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), @@ -346,14 +462,30 @@ object Converter { 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)) + 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)) + } } } @@ -380,16 +512,23 @@ object Converter { val name = unOp.register return when { - unaryExprConstructors.containsKey(unOp.op) -> { - unaryExprConstructors[unOp.op]!!(location, type, arg, name).toAssignInst() + unaryExprConstructors.containsKey( + unOp.op + ) -> { + unaryExprConstructors.getValue(unOp.op)(location, type, arg, name).toAssignInst() + } + unOp.op == "<-" -> { + GoUnArrowExpr(location, type, arg, unOp.commaOk, name).toAssignInst() + } + else -> { + GoNullInst(location.method) } - 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) } @@ -401,22 +540,57 @@ object Converter { val string = const.value return when (basicType) { - GoBasicTypes.BOOL -> GoBool(string.toBooleanStrict(), type) - GoBasicTypes.INT -> GoInt(string.toLong(), type) - GoBasicTypes.INT8 -> GoInt8(string.toByte(), type) - GoBasicTypes.INT16 -> GoInt16(string.toShort(), type) - GoBasicTypes.INT32 -> GoInt32(string.toInt(), type) - GoBasicTypes.INT64 -> GoInt64(string.toLong(), type) - GoBasicTypes.UINT -> GoUInt(string.toULong(), type) - GoBasicTypes.UINT8 -> GoUInt8(string.toUByte(), type) - GoBasicTypes.UINT16 -> GoUInt16(string.toUShort(), type) - GoBasicTypes.UINT32 -> GoUInt32(string.toUInt(), type) - GoBasicTypes.UINT64, GoBasicTypes.UINTPTR -> GoUInt64(string.toULong(), type) - GoBasicTypes.FLOAT32 -> GoFloat32(string.toFloat(), type) - GoBasicTypes.FLOAT64 -> GoFloat64(string.toDouble(), type) - GoBasicTypes.RUNE -> GoInt32(string.toInt(), type) - GoBasicTypes.STRING -> GoStringConstant(unquote(string), type) - else -> GoNullConstant(basicType) + 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) + } } } @@ -424,14 +598,28 @@ object Converter { return types.map { unpack(typesMap, it) } } - private fun unpack(types: Map, type: Type): GoType = internType( - when (type) { - is Type.Alias -> unpack(types, type.from) - is Type.Array -> ArrayType(type.len, unpack(types, type.elem)) - is Type.Basic -> unpack(type.name) - is Type.Chan -> ChanType(type.dir.toLong(), unpack(types, type.elem)) - is Type.Interface -> InterfaceType(type.methods, type.name) - is Type.Map -> MapType(unpack(types, type.key), unpack(types, type.elem)) + private fun unpack(types: Map, type: Type): GoType { + typesMap[type.name]?.let { return it } + + val result = 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 @@ -439,74 +627,127 @@ object Converter { named } - is Type.Opaque -> OpaqueType(type.name) - is Type.Pointer -> PointerType(unpack(types, type.elem)) + 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))) + 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.Slice -> SliceType(unpack(types, type.elem)) - is Type.Struct -> StructType(unpack(types, type.fields), null) - is Type.Tuple -> TupleType(unpack(types, type.elems)) - is Type.TypeParam -> TypeParam(type.name) - is Type.Union -> UnionType(emptyList()) // TODO(buraindo) proper type list when generics are supported - }, - ) + is Type.Union -> { + UnionType(emptyList()) // TODO(buraindo) proper type list when generics are supported + } + } + typesMap[type.name] = result + return result + } private fun unpack(types: Map, type: String): GoType { if (type in typesMap) { - return typesMap[type]!! + return typesMap.getValue(type) } - return unpack(types, types[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") + GoTypes.BOOL -> { + GoBasicTypes.BOOL + } + GoTypes.INT -> { + GoBasicTypes.INT + } + GoTypes.INT8 -> { + GoBasicTypes.INT8 + } + GoTypes.INT16 -> { + GoBasicTypes.INT16 + } + GoTypes.INT32 -> { + GoBasicTypes.INT32 + } + GoTypes.INT64 -> { + GoBasicTypes.INT64 + } + GoTypes.UINT -> { + GoBasicTypes.UINT + } + GoTypes.UINT8 -> { + GoBasicTypes.UINT8 + } + GoTypes.UINT16 -> { + GoBasicTypes.UINT16 + } + GoTypes.UINT32 -> { + GoBasicTypes.UINT32 + } + GoTypes.UINT64 -> { + GoBasicTypes.UINT64 + } + GoTypes.FLOAT32 -> { + GoBasicTypes.FLOAT32 + } + GoTypes.FLOAT64 -> { + GoBasicTypes.FLOAT64 + } + GoTypes.STRING -> { + GoBasicTypes.STRING + } + GoTypes.BYTE -> { + GoBasicTypes.UINT8 + } + GoTypes.RUNE -> { + GoBasicTypes.RUNE + } + GoTypes.UINTPTR -> { + GoBasicTypes.UINTPTR + } + GoTypes.UNTYPED_BOOL -> { + GoBasicTypes.BOOL + } + GoTypes.UNTYPED_INT -> { + GoBasicTypes.INT + } + GoTypes.UNTYPED_RUNE -> { + GoBasicTypes.RUNE + } + GoTypes.UNTYPED_FLOAT -> { + GoBasicTypes.FLOAT64 + } + GoTypes.UNTYPED_STRING -> { + GoBasicTypes.STRING + } + GoTypes.UNSAFE_POINTER -> { + GoBasicTypes.UNSAFE_POINTER + } + else -> { + BasicType("unknown") + } } - private fun getType(type: String): GoType = typesMap[type]!! + private fun getType(type: String): GoType = typesMap.getValue(type) private fun functionAlias(name: String): GoFunction { - return GoFunction(OpaqueType(name), emptyList(), name, emptyList(), "", emptyList(), emptyList()) + return GoFunction(OpaqueType(name), emptyList(), name, emptyList(), packageName = "", emptyList(), emptyList()) } private fun unsupportedInstruction(parent: GoMethod): GoInst { return GoNullInst(parent) } - - private val internTypeMap = mutableMapOf() - - private fun internType(type: GoType): GoType { - return internTypeMap.computeIfAbsent(type) { type } - } - - private fun unquote(s: String): String { - if (s.startsWith("\"") && s.endsWith("\"")) { - return s.substring(1, s.length - 1) - } - - return s - } } diff --git a/usvm-go/src/main/kotlin/org/usvm/model/Model.kt b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt index ce4d561b92..b289942111 100644 --- a/usvm-go/src/main/kotlin/org/usvm/model/Model.kt +++ b/usvm-go/src/main/kotlin/org/usvm/model/Model.kt @@ -8,6 +8,7 @@ data class Package( val name: String, val members: List, val types: Map, + @SerialName("int_size") val intSize: Int, ) @Serializable @@ -55,6 +56,7 @@ sealed interface Member { @Serializable data class NamedConstValue( + val bytes: String? = null, val type: String, val value: String, ) @@ -597,6 +599,7 @@ sealed interface Type { data class Pointer( override val name: String, val elem: String, + val methods: List = emptyList(), ) : Type @Serializable diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt index 420d3bed30..256b86f05c 100644 --- a/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoBinaryOperator.kt @@ -1,6 +1,5 @@ package org.usvm.operator -import io.ksmt.utils.asExpr import io.ksmt.utils.cast import org.usvm.GoContext import org.usvm.UAddressSort @@ -10,7 +9,6 @@ import org.usvm.UExpr import org.usvm.UFpSort import org.usvm.USort import org.usvm.type.goCtx -import org.usvm.uctx sealed class GoBinaryOperator( val onBool: GoContext.(UExpr, UExpr) -> UExpr = shouldNotBeCalled, @@ -59,18 +57,21 @@ sealed class GoBinaryOperator( ) data object Shl : GoBinaryOperator( - onBv = { arg, shift -> mkBvShiftLeftExpr(arg, normalizeBvShift(shift)) } + onBv = { arg, shift -> mkBvShiftLeftExpr(arg, shift) } ) class Shr(signed: Boolean) : GoBinaryOperator( onBv = { arg, shift -> - if (signed) mkBvArithShiftRightExpr(arg, normalizeBvShift(shift)) - else mkBvLogicalShiftRightExpr(arg, normalizeBvShift(shift)) + if (signed) { + mkBvArithShiftRightExpr(arg, shift) + } else { + mkBvLogicalShiftRightExpr(arg, shift) + } } ) data object AndNot : GoBinaryOperator( - onBv = { lhs, rhs -> mkBvAndExpr(lhs, mkBvNegationExpr(rhs)) } + onBv = { lhs, rhs -> mkBvAndExpr(lhs, mkBvNotExpr(rhs)) } ) data object Eql : GoBinaryOperator( @@ -112,33 +113,29 @@ sealed class GoBinaryOperator( 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") + lhsSort != rhsSort -> { + error("Expressions sorts mismatch: $lhsSort, $rhsSort") + } + lhsSort is UBoolSort -> { + lhs.goCtx.onBool(lhs.cast(), rhs.cast()) + } + lhsSort is UBvSort -> { + lhs.goCtx.onBv(lhs.cast(), rhs.cast()) + } + lhsSort is UFpSort -> { + lhs.goCtx.onFp(lhs.cast(), rhs.cast()) + } + lhsSort is UAddressSort -> { + lhs.goCtx.onAddress(lhs.cast(), rhs.cast()) + } + else -> { + error("Unexpected sorts: $lhsSort, $rhsSort") + } } } companion object { private val shouldNotBeCalled: GoContext.(UExpr, UExpr) -> UExpr = { _, _ -> error("Should not be called") } - - internal fun normalizeBvShift(shift: UExpr): UExpr = with(shift.uctx) { - return when (shift.sort) { - bv32Sort -> { - val mask = mkBv(31) // 0b11111 - mkBvAndExpr(shift.asExpr(bv32Sort), mask).asExpr(shift.sort) - } - - bv64Sort -> { - val mask = mkBv(63L) // 0b111111 - mkBvAndExpr(shift.asExpr(bv64Sort), mask).asExpr(shift.sort) - } - - else -> error("Incorrect bv shift: $shift") - } - } } } diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt index bb251e9bfd..efa83c80e2 100644 --- a/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoOperatorUtils.kt @@ -15,11 +15,13 @@ private val intSizeBits = Int.SIZE_BITS.toUInt() 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) - ) + boolSort -> { + mkIte( + this@wideTo32BitsIfNeeded as UExpr, + mkBv(1, intSizeBits), + mkBv(0, intSizeBits) + ) + } is UBvSort -> { if (sort.sizeBits < intSizeBits) { @@ -29,8 +31,12 @@ internal fun UExpr.wideTo32BitsIfNeeded(signed: Boolean): UExpr this@wideTo32BitsIfNeeded - else -> error("Unexpected sort: $sort") + is UFpSort -> { + this@wideTo32BitsIfNeeded + } + else -> { + error("Unexpected sort: $sort") + } } } diff --git a/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt b/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt index 2d6ff382fb..6bc34f2661 100644 --- a/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt +++ b/usvm-go/src/main/kotlin/org/usvm/operator/GoUnaryOperator.kt @@ -21,6 +21,10 @@ sealed class GoUnaryOperator( onFp = GoContext::mkFpNegationExpr, ) + data object Complement : GoUnaryOperator( + onBv = GoContext::mkBvNotExpr, + ) + data object CastToBool : GoUnaryOperator( onBool = { it }, onBv = { operand -> operand neq mkBv(0, operand.sort) } @@ -60,10 +64,18 @@ sealed class GoUnaryOperator( 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") + 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 { 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/GoMapKeys.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoMapKeys.kt new file mode 100644 index 0000000000..bcf0236eac --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoMapKeys.kt @@ -0,0 +1,80 @@ +package org.usvm.state + +import org.jacodb.go.api.GoType +import org.jacodb.go.api.MapType +import org.usvm.GoContext +import org.usvm.UBoolExpr +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.USizeSort +import org.usvm.USort +import org.usvm.api.refSetContainsElement +import org.usvm.collection.field.UFieldLValue +import org.usvm.collection.map.length.UMapLengthLValue +import org.usvm.collection.map.ref.URefMapEntryLValue +import org.usvm.memory.UReadOnlyMemory +import org.usvm.mkSizeAddExpr +import org.usvm.mkSizeExpr +import org.usvm.mkSizeGeExpr +import org.usvm.sizeSort +import org.usvm.type.needsMapKeyRepresentative + +/** Canonical identities and observed membership must agree with the declared map size. */ +internal fun GoState.mapKeyCanonicalityConstraint( + map: UHeapRef, + type: MapType, + sourceMemory: UReadOnlyMemory = memory, +): UBoolExpr = with(ctx) { + val usesRepresentatives = type.keyType.needsMapKeyRepresentative() + var result: UBoolExpr = if (usesRepresentatives) { + mkNot(sourceMemory.refSetContainsElement(map, nullRef, type)) + } else { + trueExpr + } + var minimumSize: UExpr = mkSizeExpr(0) + val previousEntries = mutableListOf() + for (entry in data.mapKeyRepresentatives[type.keyType].orEmpty()) { + val canonical = mkHeapRefEq(entry.reference, entry.canonicalReference) + val present = sourceMemory.refSetContainsElement(map, entry.reference, type) + val valid = mkOr(mkNot(entry.guard), canonical, mkNot(present)) + result = mkAnd(result, valid) + + if (usesRepresentatives) { + // Logical nil keys live in a nonnull proxy; inactive range proxies cannot add null entries. + val nonnullProxy = mkNot(mkHeapRefEq(entry.reference, nullRef)) + result = mkAnd(result, mkOr(mkNot(present), nonnullProxy)) + } + + val previousMatches = previousEntries.map { previous -> + mkAnd(previous.guard, mkHeapRefEq(previous.reference, entry.reference)) + } + val alreadyCounted = mkOr(previousMatches) + val contributes = mkAnd(entry.guard, present, mkNot(alreadyCounted)) + minimumSize = mkIte(contributes, mkSizeAddExpr(minimumSize, mkSizeExpr(1)), minimumSize) + previousEntries += entry + } + val declaredSize = sourceMemory.read(UMapLengthLValue(map, type, sizeSort)) + mkAnd(result, mkSizeGeExpr(declaredSize, minimumSize)) +} + +/** Storage identity and the supplied Go value have separate responsibilities. */ +internal data class GoResolvedMapKey(val key: UExpr, val snapshot: UExpr) + +/** Separate region: actual per-map key payloads cannot collide with map values. */ +internal data class GoMapKeyPayloadRegion(val mapType: MapType) + +internal fun GoContext.mapKeyPayloadLValue( + map: UHeapRef, + key: UHeapRef, + type: MapType, +): URefMapEntryLValue = + URefMapEntryLValue(typeToSort(type.keyType), map, key, GoMapKeyPayloadRegion(type)) + +/** Internal proxy payloads never share user tuple or boxed-value field indexes. */ +internal data class GoMapKeySnapshotField(val keyType: GoType) + +internal fun GoContext.mapKeySnapshotLValue( + key: UHeapRef, + type: GoType, +): UFieldLValue = + UFieldLValue(typeToSort(type), key, GoMapKeySnapshotField(type)) diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt index 937ffe4185..bf772bb270 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoState.kt @@ -2,6 +2,7 @@ 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 @@ -14,9 +15,14 @@ import org.jacodb.go.api.GoInst import org.jacodb.go.api.GoMethod import org.jacodb.go.api.GoParameter import org.jacodb.go.api.GoType +import org.jacodb.go.api.InterfaceType +import org.jacodb.go.api.MapType import org.jacodb.go.api.NamedType +import org.jacodb.go.api.NullType import org.jacodb.go.api.PointerType +import org.jacodb.go.api.SignatureType import org.jacodb.go.api.SliceType +import org.jacodb.go.api.StructType import org.usvm.GoCall import org.usvm.GoContext import org.usvm.GoTarget @@ -26,12 +32,10 @@ import org.usvm.UCallStack import org.usvm.UConcreteHeapRef import org.usvm.UExpr import org.usvm.UHeapRef -import org.usvm.ULValuePointer import org.usvm.UNullRef import org.usvm.USort import org.usvm.UState import org.usvm.api.readField -import org.usvm.api.writeArrayLength import org.usvm.api.writeField import org.usvm.collection.field.UFieldLValue import org.usvm.collections.immutable.internal.MutabilityOwnership @@ -39,6 +43,8 @@ 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 @@ -47,8 +53,6 @@ import org.usvm.sizeSort import org.usvm.targets.UTargetsSet import org.usvm.type.GoBasicTypes import org.usvm.type.underlying -import org.usvm.memory.allocateGoArray as allocateArray -import org.usvm.memory.allocateGoArrayInitialized as allocateArrayInitialized class GoState( ctx: GoContext, @@ -84,7 +88,7 @@ class GoState( this.ownership = newThisOwnership return GoState( ctx, - ownership, + cloneOwnership, entrypoint, callStack.clone(), clonedConstraints, @@ -127,7 +131,7 @@ class GoState( val mergedTargets = targets.takeIf { it == other.targets } ?: return null mergedPathConstraints += ctx.mkOr(mergeGuard.thisConstraint, mergeGuard.otherConstraint) - val mergedData = data.mergeWith(other.data) + val mergedData = data.mergeWith(other.data) ?: return null return GoState( ctx, @@ -160,6 +164,7 @@ class GoState( fun returnValue(valueToReturn: UExpr, type: GoType) { val returnFromMethod = lastEnteredMethod val returnSite = callStack.pop() + data.popDeferredFrame() if (callStack.isNotEmpty()) { memory.stack.pop() } @@ -179,6 +184,7 @@ class GoState( require(methodResult is GoMethodResult.Panic) val returnSite = callStack.pop() + data.popDeferredFrame() if (callStack.isNotEmpty()) { memory.stack.pop() } @@ -208,13 +214,16 @@ class GoState( data.flowStack.add(GoFlowStatus.DEFER) } + fun createCall(method: GoMethod, entrypoint: GoInst, arguments: List>): GoCall { + val copiedArguments = arguments.mapIndexed { index, value -> + copyValue(value, method.parameters[index].type as GoType) + } + val freeVariables = (method as? GoFunction)?.freeVars.orEmpty().map(::findParam) + return GoCall(method, entrypoint, copiedArguments, freeVariables) + } + fun addCall(call: GoCall, returnInst: GoInst? = null) = with(ctx) { val methodInfo = getMethodInfo(call.method) - val freeVariables = mutableListOf>().also { - if (call.method is GoFunction) { - call.method.freeVars.forEach { variable -> it.add(findParam(variable)) } - } - } val parameters = mutableListOf().also { if (call.method is GoFunction) { it.addAll(call.method.parameters) @@ -223,22 +232,35 @@ class GoState( data.flowStack.add(GoFlowStatus.NORMAL) callStack.push(call.method, returnInst) - if (methodInfo.arguments.isEmpty()) { + data.pushDeferredFrame() + if (call.arguments == null) { memory.stack.push(methodInfo.argumentsCount, methodInfo.variablesCount) } else { - memory.stack.push(methodInfo.arguments, methodInfo.variablesCount) + memory.stack.push(call.arguments.toTypedArray(), methodInfo.variablesCount) } parameters.forEachIndexed { i, parameter -> - when (val type = parameter.type) { + val parameterType = parameter.type as GoType + val value = memory.read(URegisterStackLValue(typeToSort(parameterType), i)) + pathConstraints += valueShape(value, parameterType) + + when (val type = parameterType.underlying()) { is ArrayType -> { val ref = memory.read(URegisterStackLValue(addressSort, i)).asExpr(addressSort) - memory.writeArrayLength(ref, mkSizeExpr(type.len.toInt()), type, sizeSort) + memory.writeGoArrayLength(ref, mkSizeExpr(type.len.toInt()), type, sizeSort) } } } - freeVariables.forEachIndexed { i, variable -> + 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) } @@ -247,24 +269,73 @@ class GoState( } fun deref(pointer: UHeapRef, sort: USort): UExpr { - val lvaluePointer = tryLValuePointer(pointer) - if (lvaluePointer != null) { - return memory.read(lvaluePointer.lvalue) + if (pointer is KIteExpr) { + return ctx.mkIte( + pointer.condition, + deref(pointer.trueBranch, sort).asExpr(sort), + deref(pointer.falseBranch, sort).asExpr(sort) + ) } - - val index = 0 - return memory.readField(pointer, index, 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) { - val lvaluePointer = tryLValuePointer(pointer) - if (lvaluePointer != null) { - memory.write(lvaluePointer.lvalue.withSort(rvalue.sort), rvalue.asExpr(rvalue.sort), ctx.trueExpr) + 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) + } + } - val index = 0 - memory.writeField(pointer, index, rvalue.sort, rvalue.asExpr(rvalue.sort), ctx.trueExpr) + 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 { @@ -286,9 +357,8 @@ class GoState( } fun box(expr: UExpr, targetType: GoType): UHeapRef { - val alreadyBoxed = expr.sort == ctx.addressSort && expr !is UNullRef && - expr !is KConst<*> && isBoxedConcrete(expr.asExpr(ctx.addressSort)) - if (alreadyBoxed) { + 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) } @@ -309,7 +379,17 @@ class GoState( } fun mkPointer(type: GoType, lvalue: ULValue<*, *>): UExpr { - return mkPointer(type, ULValuePointer(ctx, lvalue)) + 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) { @@ -320,35 +400,39 @@ class GoState( return ref } - fun mkString(value: String): UExpr { - return memory.allocateArrayInitialized( + 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.toByteArray().map { ctx.mkBv(it) }.asSequence() + value.map { ctx.mkBv(it) }.asSequence() ) } - private fun sampleValue(type: GoType): UExpr = when (type) { + fun sampleValue(type: GoType): UExpr = when (type) { is ArrayType -> { val sort = ctx.typeToSort(type.elementType) val contents = Array(type.len.toInt()) { sampleValue(type.elementType).asExpr(sort) }.asSequence() - memory.allocateArrayInitialized(type, sort, ctx.sizeSort, contents) + memory.allocateGoArrayInitialized(type, sort, ctx.sizeSort, contents) } - is SliceType -> memory.allocateArray(type, ctx.sizeSort, ctx.mkSizeExpr(0)) - is BasicType -> ctx.typeToSort(type).sampleUValue() - is NamedType -> box(sampleValue(type.underlyingType), type) - else -> memory.allocConcrete(type) - } - - private fun tryLValuePointer(pointer: UHeapRef): ULValuePointer? { - val index = 0 - val payload = memory.readField(pointer, index, ctx.addressSort) - if (payload is ULValuePointer) { - return payload + 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) } - return null } private fun findParam(freeVar: GoFreeVar): UExpr { @@ -364,8 +448,7 @@ class GoState( registers.pop() } - - throw IllegalStateException("param not found") + error("param not found") } private fun findParam(method: GoMethod, freeVar: GoFreeVar): Pair? { diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt index 944c327eec..4d4fa7cb0a 100644 --- a/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoStateData.kt @@ -1,7 +1,13 @@ package org.usvm.state -import org.jacodb.go.api.GoMethod +import org.jacodb.go.api.GoType import org.usvm.GoCall +import org.usvm.UBoolExpr +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, @@ -12,26 +18,62 @@ enum class GoFlowStatus { class GoStateData( val flowStack: ArrayDeque = ArrayDeque(), ) { - private val deferredCalls: MutableMap> = hashMapOf() + internal val arrayViews: MutableMap = hashMapOf() + + internal val pointerTargets: MutableMap> = hashMapOf() + + internal val pointerConversions: MutableMap = hashMapOf() + + // Lists are replaced on append, so cloned states never share mutable key histories. + internal val mapKeyRepresentatives: MutableMap> = hashMapOf() + + internal var pendingArrayCopy: GoArrayCopyOperation? = null + internal var builtinResult: UExpr? = null + + private val deferredCalls: ArrayDeque> = ArrayDeque() val flowStatus: GoFlowStatus get() = flowStack.last() - fun getDeferredCalls(method: GoMethod): ArrayDeque = deferredCalls[method] ?: ArrayDeque() + fun pushDeferredFrame() { + deferredCalls.addLast(ArrayDeque()) + } + + fun popDeferredFrame() { + deferredCalls.removeLast() + } + + fun getDeferredCalls(): ArrayDeque = deferredCalls.last() + + fun addDeferredCall(call: GoCall) { + getDeferredCalls().addLast(call) + } - fun addDeferredCall(method: GoMethod, call: GoCall) { - if (method !in deferredCalls) { - deferredCalls[method] = ArrayDeque() - } - deferredCalls[method]!!.addLast(call) + fun clone(): GoStateData = GoStateData(clonedFlowStack()).also { + it.pointerTargets.putAll(pointerTargets) + it.pointerConversions.putAll(pointerConversions) + it.arrayViews.putAll(arrayViews) + it.mapKeyRepresentatives.putAll(mapKeyRepresentatives) + it.pendingArrayCopy = pendingArrayCopy + it.builtinResult = builtinResult + deferredCalls.forEach { calls -> it.deferredCalls.addLast(ArrayDeque(calls)) } } - fun clone(): GoStateData = GoStateData(clonedFlowStack()).mergeWith(this) + 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 - fun mergeWith(other: GoStateData) = GoStateData(clonedFlowStack()).also { - for (entry in other.deferredCalls) { - entry.value.forEach { c -> it.addDeferredCall(entry.key, c) } - } + val sameFlow = flowStack.toList() == other.flowStack.toList() && sameCalls + val sameHeapViews = arrayViews == other.arrayViews && pointerTargets == other.pointerTargets + val sameKeyAndPointerHistory = pointerConversions == other.pointerConversions && + mapKeyRepresentatives == other.mapKeyRepresentatives + val canMerge = sameFlow && sameHeapViews && sameKeyAndPointerHistory + if (!canMerge) return null + + return clone() } private fun clonedFlowStack(): ArrayDeque { @@ -40,3 +82,16 @@ class GoStateData( return newStack } } + +internal data class GoPointerConversion( + val source: UHeapRef, + val sourceType: GoType, + val targetType: GoType, +) + +internal data class GoMapKeyRepresentative( + val reference: UHeapRef, + val snapshot: UExpr, + val guard: UBoolExpr, + val canonicalReference: UHeapRef = reference, +) diff --git a/usvm-go/src/main/kotlin/org/usvm/state/GoUtf8.kt b/usvm-go/src/main/kotlin/org/usvm/state/GoUtf8.kt new file mode 100644 index 0000000000..08b2e26e9d --- /dev/null +++ b/usvm-go/src/main/kotlin/org/usvm/state/GoUtf8.kt @@ -0,0 +1,102 @@ +package org.usvm.state + +import io.ksmt.utils.asExpr +import org.usvm.UBoolExpr +import org.usvm.UBv32Sort +import org.usvm.UExpr +import org.usvm.UHeapRef +import org.usvm.USizeSort +import org.usvm.memory.readGoArrayIndex +import org.usvm.mkSizeAddExpr +import org.usvm.mkSizeExpr +import org.usvm.mkSizeGeExpr +import org.usvm.mkSizeSubExpr +import org.usvm.type.GoBasicTypes + +private const val MAX_UTF8_WIDTH = 4 +private const val BYTE_TO_RUNE_EXTENSION = 24 +private const val THREE_BYTE_WIDTH = 3 +private const val FOUR_BYTE_WIDTH = 4 +private const val THREE_BYTE_MIN_LEAD = 0xE0 +private const val SURROGATE_LEAD = 0xED +private const val FOUR_BYTE_MIN_LEAD = 0xF0 +private const val MAX_UNICODE_LEAD = 0xF4 +private const val REPLACEMENT_RUNE = 0xFFFD + +/** Go range decodes one UTF-8 rune; each malformed byte produces RuneError with width one. */ +internal fun GoState.decodeUtf8Rune( + string: UHeapRef, + index: UExpr, + length: UExpr, +): Pair, UExpr> = with(ctx) { + val bytes = (0 until MAX_UTF8_WIDTH).map { offset -> + val byte = memory.readGoArrayIndex( + string, + mkSizeAddExpr(index, mkSizeExpr(offset)), + GoBasicTypes.STRING, + bv8Sort, + ) + mkBvZeroExtensionExpr(extensionSize = BYTE_TO_RUNE_EXTENSION, value = byte).asExpr(bv32Sort) + } + val first = bytes[0] + val remaining = mkSizeSubExpr(length, index) + fun between(value: UExpr, low: Int, high: Int): UBoolExpr = mkAnd( + mkBvUnsignedGreaterOrEqualExpr(value, mkBv(low)), + mkBvUnsignedLessOrEqualExpr(value, mkBv(high)), + ) + fun continuation(offset: Int): UBoolExpr = between(bytes[offset], low = 0x80, high = 0xBF) + fun payload(offset: Int, mask: Int, shift: Int): UExpr = + mkBvShiftLeftExpr(mkBvAndExpr(bytes[offset], mkBv(mask)), mkBv(shift)) + + val ascii = between(first, low = 0, high = 0x7F) + val twoBytes = mkAnd( + mkSizeGeExpr(remaining, mkSizeExpr(2)), + between(first, low = 0xC2, high = 0xDF), + continuation(offset = 1), + ) + val validThreeByteSecond = mkAnd( + continuation(offset = 1), + mkOr(mkNot(mkEq(first, mkBv(THREE_BYTE_MIN_LEAD))), between(bytes[1], low = 0xA0, high = 0xBF)), + mkOr(mkNot(mkEq(first, mkBv(SURROGATE_LEAD))), between(bytes[1], low = 0x80, high = 0x9F)), + ) + val threeBytes = mkAnd( + mkSizeGeExpr(remaining, mkSizeExpr(THREE_BYTE_WIDTH)), + between(first, low = 0xE0, high = 0xEF), + validThreeByteSecond, + continuation(offset = 2), + ) + val validFourByteSecond = mkAnd( + continuation(offset = 1), + mkOr(mkNot(mkEq(first, mkBv(FOUR_BYTE_MIN_LEAD))), between(bytes[1], low = 0x90, high = 0xBF)), + mkOr(mkNot(mkEq(first, mkBv(MAX_UNICODE_LEAD))), between(bytes[1], low = 0x80, high = 0x8F)), + ) + val fourBytes = mkAnd( + mkSizeGeExpr(remaining, mkSizeExpr(FOUR_BYTE_WIDTH)), + between(first, low = 0xF0, high = 0xF4), + validFourByteSecond, + continuation(offset = 2), + continuation(offset = 3), + ) + + val rune2 = mkBvOrExpr(payload(offset = 0, mask = 0x1F, shift = 6), payload(offset = 1, mask = 0x3F, shift = 0)) + val rune3 = mkBvOrExpr( + payload(offset = 0, mask = 0x0F, shift = 12), + mkBvOrExpr(payload(offset = 1, mask = 0x3F, shift = 6), payload(offset = 2, mask = 0x3F, shift = 0)), + ) + val rune4 = mkBvOrExpr( + payload(offset = 0, mask = 0x07, shift = 18), + mkBvOrExpr( + payload(offset = 1, mask = 0x3F, shift = 12), + mkBvOrExpr(payload(offset = 2, mask = 0x3F, shift = 6), payload(offset = 3, mask = 0x3F, shift = 0)), + ), + ) + val rune4OrError = mkIte(fourBytes, rune4, mkBv(REPLACEMENT_RUNE)) + val multibyteRune = mkIte(twoBytes, rune2, mkIte(threeBytes, rune3, rune4OrError)) + val rune = mkIte(ascii, first, multibyteRune) + + val fourByteOrInvalidWidth = mkIte(fourBytes, mkSizeExpr(FOUR_BYTE_WIDTH), mkSizeExpr(1)) + val longWidth = mkIte(threeBytes, mkSizeExpr(THREE_BYTE_WIDTH), fourByteOrInvalidWidth) + val width = mkIte(twoBytes, mkSizeExpr(2), longWidth) + + rune to width +} 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/GoMapKeyType.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoMapKeyType.kt index 3102de09a9..ef083d4e14 100644 --- a/usvm-go/src/main/kotlin/org/usvm/type/GoMapKeyType.kt +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoMapKeyType.kt @@ -1,7 +1,11 @@ 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.PointerType +import org.jacodb.go.api.StructType /** Named numeric and boolean keys use their scalar payload, not their box identity. */ internal fun GoType.mapKeyStorageType(): GoType { @@ -10,3 +14,9 @@ internal fun GoType.mapKeyStorageType(): GoType { return if (isScalar) underlying else this } + +/** These keys need Go value equality instead of SMT scalar or heap reference equality. */ +internal fun GoType.needsMapKeyRepresentative(): Boolean = when (val type = underlying()) { + is ArrayType, is StructType, is InterfaceType, is PointerType -> true + else -> type == GoBasicTypes.STRING || type == GoBasicTypes.FLOAT32 || type == GoBasicTypes.FLOAT64 +} diff --git a/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt index 4177c70701..527c8f0f61 100644 --- a/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoType.kt @@ -9,7 +9,6 @@ import io.ksmt.sort.KSortVisitor import org.jacodb.go.api.BasicType import org.jacodb.go.api.GoType import org.jacodb.go.api.NamedType -import org.jacodb.go.api.SliceType import org.usvm.GoContext import org.usvm.UExpr import org.usvm.USort @@ -57,14 +56,18 @@ object GoBasicTypes { val UINTPTR = BasicType(GoTypes.UINTPTR) val FLOAT32 = BasicType(GoTypes.FLOAT32) val FLOAT64 = BasicType(GoTypes.FLOAT64) - val STRING = SliceType(UINT8) + 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 + is NamedType -> { + this.underlyingType.underlying() + } + else -> { + this + } } class GoVoidSort(ctx: GoContext) : USort(ctx) { diff --git a/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt index 8930ef63ef..77ab552478 100644 --- a/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt +++ b/usvm-go/src/main/kotlin/org/usvm/type/GoTypeSystem.kt @@ -7,10 +7,8 @@ import org.jacodb.go.api.InterfaceType import org.jacodb.go.api.MapType import org.jacodb.go.api.NamedType import org.jacodb.go.api.PointerType -import org.jacodb.go.api.SignatureType import org.jacodb.go.api.SliceType import org.jacodb.go.api.StructType -import org.jacodb.go.api.TupleType import org.usvm.types.USupportTypeStream import org.usvm.types.UTypeStream import org.usvm.types.UTypeSystem @@ -19,6 +17,7 @@ import kotlin.time.Duration class GoTypeSystem( override val typeOperationsTimeout: Duration, val types: Collection, + private val methodSets: Map>, ) : UTypeSystem { private val goAnyType = InterfaceType(emptyList(), "any") private val topTypeStream by lazy { USupportTypeStream.from(this, goAnyType) } @@ -32,40 +31,66 @@ class GoTypeSystem( } override fun isInstantiable(type: GoType): Boolean = when (type) { - is StructType, is MapType, is SliceType, is ArrayType, is BasicType -> true - is NamedType -> isInstantiable(type.underlyingType) - is PointerType -> isInstantiable(type.baseType) - else -> false + 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 BasicType, is ArrayType, is SliceType, is MapType, is PointerType, is SignatureType, is TupleType -> true - else -> false + is InterfaceType -> false + is NamedType -> type.underlying() !is InterfaceType + else -> true } - override fun hasCommonSubtype(type: GoType, types: Collection): Boolean = when (type) { - is BasicType, is ArrayType, is SliceType, is MapType, is PointerType, is SignatureType, is TupleType -> { - types.isEmpty() + 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) } } - is InterfaceType -> types.none { !isFinal(it) } - is NamedType -> hasCommonSubtype(type.underlyingType, types) - is StructType -> types.all { it is InterfaceType || isSupertype(it, type) } - else -> false } override fun isSupertype(supertype: GoType, type: GoType): Boolean = when { - supertype == type -> true + // 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) + isSupertype( + supertype.underlyingType, + type + ) + } + supertype is InterfaceType -> { + implements(supertype, type) + } + else -> { + false } - supertype is InterfaceType -> implements(supertype, type) - else -> false } private fun implements(iface: InterfaceType, impl: GoType): Boolean = when { - iface.methods.isEmpty() -> true - impl is NamedType -> impl.methods.containsAll(iface.methods) - impl is PointerType -> implements(iface, impl.baseType) - else -> false + 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/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/JacoDbTest.kt b/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt deleted file mode 100644 index dd034f945a..0000000000 --- a/usvm-go/src/test/kotlin/org/usvm/JacoDbTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -package org.usvm - -import org.jacodb.go.api.GoMethod -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.TestFactory -import org.usvm.model.Converter -import org.usvm.model.Parser -import java.nio.file.Files -import java.nio.file.Paths -import java.util.stream.Collectors -import kotlin.io.path.extension -import kotlin.io.path.pathString -import kotlin.system.measureTimeMillis -import kotlin.time.Duration.Companion.seconds - -class JacoDbTest { - @TestFactory - fun jacodbTestLong(): Collection { - val pkg = Converter.unpack(Parser().deserialize("out/usvm_examples.json")) - val program = GoProgram(listOf(pkg)) - - return methods(pkg).filter { it.metName in longMethods }.map { method -> - DynamicTest.dynamicTest(method.metName) { - GoMachine(program, options, customOptions).use { machine -> - analyzeAndLog(machine, pkg, method.metName) - } - } - } - } - - @TestFactory - fun jacodbTestFast(): Collection { - val pkg = Converter.unpack(Parser().deserialize("out/usvm_examples.json")) - val program = GoProgram(listOf(pkg)) - - return methods(pkg) - .filter { it.metName !in longMethods } - .let { if (whitelist.isEmpty()) it else it.filter { m -> m.metName in whitelist } } - .map { method -> - DynamicTest.dynamicTest(method.metName) { - GoMachine(program, options, customOptions).use { machine -> - analyzeAndLog(machine, pkg, method.metName) - } - } - } - } - - @TestFactory - fun jacodbTestImports(): Collection { - val packages = Files.list(Paths.get("out")).use { paths -> - paths.filter { path -> path.extension != "yaml" } - .map { Converter.unpack(Parser().deserialize(it.pathString)) } - .collect(Collectors.toList()) - } - val program = GoProgram(packages) - val pkg = program.findPackage("usvm/examples/imports") - - return methods(pkg).filter { it.metName !in longMethods }.map { method -> - DynamicTest.dynamicTest(method.metName) { - GoMachine(program, options, customOptions).use { machine -> - analyzeAndLog(machine, pkg, method.metName) - } - } - } - } - - @TestFactory - fun jacodbTestStdStrings(): Collection { - val packages = Files.list(Paths.get("out")).use { paths -> - paths.filter { path -> path.extension != "yaml" } - .map { Converter.unpack(Parser().deserialize(it.pathString)) } - .collect(Collectors.toList()) - } - val program = GoProgram(packages) - val pkg = program.findPackage("flag") - - return methods(pkg).filter { it.metName !in longMethods }.map { method -> - DynamicTest.dynamicTest(method.metName) { - GoMachine(program, options, customOptions).use { machine -> - analyzeAndLog(machine, pkg, method.metName) - } - } - } - } - - private fun analyzeAndLog(machine: GoMachine, pkg: GoPackage, methodName: String) { - val elapsedMillis = measureTimeMillis { - val result = machine.analyzeAndResolve(pkg, methodName) - logger.info { result } - } - logger.info { "Analysis took $elapsedMillis ms" } - } - - private fun methods(pkg: GoPackage): List = pkg.methods.filter { !it.metName.contains('$') } - - private val options: UMachineOptions = UMachineOptions( - pathSelectionStrategies = listOf(PathSelectionStrategy.FORK_DEPTH), - coverageZone = CoverageZone.TRANSITIVE, - exceptionsPropagation = true, - timeout = 30.seconds, - stepsFromLastCovered = 1_000_000L, - solverTimeout = 5.seconds, - typeOperationsTimeout = 5.seconds, - ) - - private val customOptions: GoMachineOptions = GoMachineOptions( - failOnNotFullCoverage = false, - uncoveredMethods = listOf("panicRecoverComplex"), - ) - - private val longMethods = arrayOf("loopInfinite", "loopInner", "loopCollatz", "mapLoopLen", "canVisitAllRooms") - - private val whitelist = arrayOf( -// "sliceCustomAppend" -// "appendErrorStrings" -// "(usvm/examples.Person).WithName" -// "sliceCustomAppend" -// "shiftErrorToString" -// "mapCustomDeleteSimple" - ) -} 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..6ccf609237 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/collections/maps/MapValueRegressionTest.kt @@ -0,0 +1,435 @@ +package org.usvm.samples.collections.maps + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put +import org.junit.jupiter.api.Test +import org.usvm.generatedGoFile +import org.usvm.interpreter.GoMapEntries +import org.usvm.interpreter.GoPointer +import org.usvm.samples.GoExecution +import org.usvm.samples.GoNativeTestRunner +import org.usvm.samples.GoResult +import org.usvm.samples.GoSample +import org.usvm.samples.longEntry +import org.usvm.samples.replayWithNativeGo +import org.usvm.test.util.checkers.eq +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class MapValueRegressionTest : GoNativeTestRunner() { + @Test + @GoSample(method = "mapKeyHashAfterValueEvaluation", fixture = "regressions") + fun mapKeyHashAfterValueEvaluation() { + // The unhashable-key panic makes the normal assignment tail unreachable. + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + checkNative(method = "mapKeyHashAfterValueEvaluation") + } + + @Test + @GoSample(method = "markMapKeyValue", fixture = "regressions") + fun markMapKeyValue() = checkParameterMutations( + method = "markMapKeyValue", + { execution -> + val before = execution.arguments.single() as? GoPointer + val after = execution.argumentsAfter.single() as? GoPointer + if (before == null) { + execution.result.isPanic && after == null + } else { + execution.result.long == 1L && (after?.value as? Number)?.toLong() == 7L + } + }, + ) + + @Test + @GoSample(method = "recoverMapKeyAssignment", fixture = "regressions") + fun recoverMapKeyAssignment() { + // Outside a deferred panic, recover is nil; the assignment is covered through its caller. + machineOptions = machineOptions.copy(failOnNotFullCoverage = false) + checkParameterMutations( + method = "recoverMapKeyAssignment", + { execution -> execution.result.isSuccess && execution.arguments == execution.argumentsAfter }, + ) + } + + @Test + @GoSample(method = "mapStructNaNUncomparablePanics", fixture = "regressions") + fun mapStructNaNUncomparablePanics() = checkNative(method = "mapStructNaNUncomparablePanics") + + @Test + @GoSample(method = "inputStringMapSymbolicKeyAndExtra", fixture = "regressions") + fun inputStringMapSymbolicKeyAndExtra() = replayMapInputs( + method = "inputStringMapSymbolicKeyAndExtra", + observedKey = "", + needsAdditionalEntry = true, + ) + + @Test + @GoSample(method = "inputFloatMapCanonicalGhosts", fixture = "regressions") + fun inputFloatMapCanonicalGhosts() = replayMapInputs( + method = "inputFloatMapCanonicalGhosts", + observedKey = 0.0, + needsAdditionalEntry = true, + ) + + @Test + @GoSample(method = "floatMapZeroRangeAfterOverwrite", fixture = "regressions") + fun floatMapZeroRangeAfterOverwrite() = checkNative(method = "floatMapZeroRangeAfterOverwrite") + + @Test + @GoSample(method = "inputFloatMapZeroRangeSign", fixture = "regressions") + fun inputFloatMapZeroRangeSign() { + // IEEE division distinguishes the stored zero sign; CI needs time to solve both input witnesses. + options = options.copy(timeout = 30.seconds, solverTimeout = 10.seconds) + + replayMapInputs( + method = "inputFloatMapZeroRangeSign", + observedKey = 0.0, + expectedOutcomes = setOf(-1L, 0L, 1L), + ) + } + + @Test + @GoSample(method = "nilPointerMapKeyLookupAndRange", fixture = "regressions") + fun nilPointerMapKeyLookupAndRange() = checkNative(method = "nilPointerMapKeyLookupAndRange") + + @Test + @GoSample(method = "pointerMapConvertedAlias", fixture = "regressions") + fun pointerMapConvertedAlias() = checkNative(method = "pointerMapConvertedAlias") + + @Test + @GoSample(method = "negativeMapZero", fixture = "regressions") + fun negativeMapZero() = checkDiscoveredProperties( + method = "negativeMapZero", + analysisResultsMatchers = arrayOf({ value: Double, result: GoResult -> + val actual = result.value as? Double + result.isSuccess && actual != null && + (value.isNaN() && actual.isNaN() || actual.toRawBits() == (-value).toRawBits()) + }), + ) + + @Test + @GoSample(method = "mapNaN", fixture = "regressions") + fun mapNaN() = checkDiscoveredProperties( + method = "mapNaN", + analysisResultsMatchers = arrayOf({ value: Double, result: GoResult -> + val expected = value / value + val actual = result.value as? Double + result.isSuccess && actual != null && (actual == expected || actual.isNaN() && expected.isNaN()) + }), + ) + + @Test + @GoSample(method = "float32MapZeroAndNaN", fixture = "regressions") + fun float32MapZeroAndNaN() = checkNative(method = "float32MapZeroAndNaN") + + @Test + @GoSample(method = "interfaceMapUncomparableLookup", fixture = "regressions") + fun interfaceMapUncomparableLookup() = checkNative(method = "interfaceMapUncomparableLookup") + + @Test + @GoSample(method = "interfaceMapUncomparableUpdate", fixture = "regressions") + fun interfaceMapUncomparableUpdate() = checkNative(method = "interfaceMapUncomparableUpdate") + + @Test + @GoSample(method = "inputFloatMapLookup", fixture = "regressions") + fun inputFloatMapLookup() = replayMapInputs(method = "inputFloatMapLookup", observedKey = 0.0) + + @Test + @GoSample(method = "inputStringMapLookup", fixture = "regressions") + fun inputStringMapLookup() = replayMapInputs(method = "inputStringMapLookup", observedKey = "same") + + @Test + @GoSample(method = "inputFloatMapExtraEntry", fixture = "regressions") + fun inputFloatMapExtraEntry() = replayMapInputs( + method = "inputFloatMapExtraEntry", + observedKey = 0.0, + needsAdditionalEntry = true, + ) + + @Test + @GoSample(method = "inputStringMapExtraEntry", fixture = "regressions") + fun inputStringMapExtraEntry() = replayMapInputs( + method = "inputStringMapExtraEntry", + observedKey = "same", + needsAdditionalEntry = true, + ) + + @Test + @GoSample(method = "floatMapNaNResult", fixture = "regressions") + fun floatMapNaNResult() { + val executions = runner("floatMapNaNResult", options) + assertEquals(expected = 1, actual = executions.size) + val entries = executions.single().result.value as GoMapEntries + assertEquals(expected = 2, actual = entries.size) + assertTrue(entries.entries.all { (it.key as Double).isNaN() }) + assertEquals(setOf(7L, 9L), entries.entries.map { (it.value as Number).toLong() }.toSet()) + assertEquals(expected = 2, actual = entries.entries.map { it.identity }.toSet().size) + + replayMapOutputs(method = "floatMapNaNResult", executions = executions, expectedSummary = 216L) + } + + @Test + @GoSample(method = "pointerMapKeyResult", fixture = "regressions") + fun pointerMapKeyResult() { + val executions = runner("pointerMapKeyResult", options) + assertEquals(expected = 1, actual = executions.size) + val entries = executions.single().result.value as GoMapEntries + assertEquals(expected = 2, actual = entries.size) + assertTrue(entries.entries.all { ((it.key as GoPointer).value as Number).toLong() == 0L }) + assertEquals(setOf(7L, 9L), entries.entries.map { (it.value as Number).toLong() }.toSet()) + assertEquals(expected = 2, actual = entries.entries.map { it.identity }.toSet().size) + + replayMapOutputs(method = "pointerMapKeyResult", executions = executions, expectedSummary = 216L) + } + + @Test + @GoSample(method = "namedStringMapResult", fixture = "regressions") + fun namedStringMapResult() { + val executions = runner("namedStringMapResult", options) + assertEquals(expected = 1, actual = executions.size) + assertEquals(mapOf("same" to 9L), executions.single().result.map) + + replayMapOutputs(method = "namedStringMapResult", executions = executions, expectedSummary = 109L) + } + + @Test + @GoSample(method = "namedArrayMapResult", fixture = "regressions") + fun namedArrayMapResult() { + val executions = runner("namedArrayMapResult", options) + assertEquals(expected = 1, actual = executions.size) + assertEquals(mapOf(listOf(1L, 2L) to 7L), executions.single().result.map) + + replayMapOutputs(method = "namedArrayMapResult", executions = executions, expectedSummary = 119L) + } + + private fun replayMapInputs( + method: String, + observedKey: Any, + needsAdditionalEntry: Boolean = false, + expectedOutcomes: Set? = null, + ) { + val executions = runner(method, options) + assertTrue(executions.isNotEmpty()) + assertTrue(executions.all { it.result.isSuccess }) + if (expectedOutcomes != null) assertEquals(expectedOutcomes, executions.map { it.result.long }.toSet()) + if (needsAdditionalEntry) { + val hasAdditionalEntry = executions.any { + val input = mapReplayEntries(it.arguments.first()) + val actualObservedKey = it.arguments.getOrNull(1) ?: observedKey + val observed = input?.firstOrNull { entry -> + val numericKey = entry.first as? Double + val numericObservedKey = actualObservedKey as? Double + if (numericKey != null && numericObservedKey != null) { + numericKey == numericObservedKey + } else { + entry.first == actualObservedKey + } + } + input?.size == 2 && observed != null && + it.result.long == (observed.second as Number).toLong() + } + assertTrue(hasAdditionalEntry, message = "A witness must retain an observed key and an unobserved entry") + } + + val inputs = buildJsonArray { + executions.forEach { execution -> + val input = mapReplayEntries(execution.arguments.first()) + add( + buildJsonObject { + put("nil", input == null) + when (val query = execution.arguments.getOrNull(1)) { + is String -> put("lookupKey", query) + is Double -> put("lookupFloatBits", query.toRawBits().toULong().toString(radix = 16)) + } + put( + "entries", + buildJsonArray { + input.orEmpty().forEach { (key, value) -> + add( + buildJsonObject { + when (key) { + is Double -> put( + "floatKeyBits", + key.toRawBits().toULong().toString(radix = 16), + ) + is String -> put("stringKey", key) + else -> error("Unsupported map replay key $key") + } + put("value", (value as Number).toLong()) + }, + ) + } + }, + ) + }, + ) + } + } + val outputs = replayMapWitnesses(method = method, inputs = inputs) + assertEquals(executions.map { it.result.long }, outputs) + } + + private fun mapReplayEntries(value: Any?): List>? = when (value) { + null -> null + is GoMapEntries -> value.entries.map { it.key to it.value } + is Map<*, *> -> value.entries.map { it.key to it.value } + else -> error("Unsupported map witness $value") + } + + private fun replayMapOutputs(method: String, executions: List, expectedSummary: Long) { + assertTrue(executions.single().result.isSuccess) + val inputs = buildJsonArray { add(buildJsonObject { put("nil", true) }) } + assertEquals(listOf(expectedSummary), replayMapWitnesses(method = method, inputs = inputs)) + } + + private fun replayMapWitnesses(method: String, inputs: JsonArray): List { + val request = buildJsonObject { + put("method", method) + put("inputs", inputs) + } + return replayWithNativeGo( + executable = generatedGoFile("native-replay.test"), + testName = "TestReplayMapKeys", + request = request, + timeout = 5.seconds, + ).jsonArray.map { it.jsonPrimitive.long } + } + + @Test + @GoSample(method = "floatMapZeroLookup", fixture = "regressions") + fun floatMapZeroLookup() = checkNative(method = "floatMapZeroLookup") + + @Test + @GoSample(method = "floatMapZeroOverwrite", fixture = "regressions") + fun floatMapZeroOverwrite() = checkNative(method = "floatMapZeroOverwrite") + + @Test + @GoSample(method = "floatMapZeroDelete", fixture = "regressions") + fun floatMapZeroDelete() = checkNative(method = "floatMapZeroDelete") + + @Test + @GoSample(method = "namedFloatMapZeroLookup", fixture = "regressions") + fun namedFloatMapZeroLookup() = checkNative(method = "namedFloatMapZeroLookup") + + @Test + @GoSample(method = "floatMapNaNLookup", fixture = "regressions") + fun floatMapNaNLookup() = checkNative(method = "floatMapNaNLookup") + + @Test + @GoSample(method = "floatMapNaNRange", fixture = "regressions") + fun floatMapNaNRange() = checkNative(method = "floatMapNaNRange") + + @Test + @GoSample(method = "stringMapValueLookup", fixture = "regressions") + fun stringMapValueLookup() = checkNative(method = "stringMapValueLookup") + + @Test + @GoSample(method = "namedStringMapValueLookup", fixture = "regressions") + fun namedStringMapValueLookup() = checkNative(method = "namedStringMapValueLookup") + + @Test + @GoSample(method = "arrayMapKeySnapshot", fixture = "regressions") + fun arrayMapKeySnapshot() = checkNative(method = "arrayMapKeySnapshot") + + @Test + @GoSample(method = "structMapValueLookup", fixture = "regressions") + fun structMapValueLookup() = checkNative(method = "structMapValueLookup") + + @Test + @GoSample(method = "arrayMapRangeKeyCopy", fixture = "regressions") + fun arrayMapRangeKeyCopy() = checkNative(method = "arrayMapRangeKeyCopy") + + @Test + @GoSample(method = "interfaceMapValueLookup", fixture = "regressions") + fun interfaceMapValueLookup() = checkNative(method = "interfaceMapValueLookup") + + @Test + @GoSample(method = "namedScalarMapLookup", fixture = "regressions") + fun namedScalarMapLookup() = checkNative(method = "namedScalarMapLookup") + + @Test + @GoSample(method = "namedScalarMapOverwrite", fixture = "regressions") + fun namedScalarMapOverwrite() = checkNative(method = "namedScalarMapOverwrite") + + @Test + @GoSample(method = "namedScalarMapDelete", fixture = "regressions") + fun namedScalarMapDelete() = checkNative(method = "namedScalarMapDelete") + + @Test + @GoSample(method = "namedScalarMapRange", fixture = "regressions") + fun namedScalarMapRange() = checkNative(method = "namedScalarMapRange") + + @Test + @GoSample(method = "namedBooleanMapKeys", fixture = "regressions") + fun namedBooleanMapKeys() = checkNative(method = "namedBooleanMapKeys") + + @Test + @GoSample(method = "namedScalarMapResult", fixture = "regressions") + fun namedScalarMapResult() { + checkMatches( + method = "namedScalarMapResult", + analysisResultsNumberMatcher = eq(count = 1), + { result: GoResult -> result.map?.size == 1 && result.map.longEntry(key = 1) == 7L }, + ) + } + + @Test + @GoSample(method = "namedUnsignedMapResult", fixture = "regressions") + fun namedUnsignedMapResult() { + checkMatches( + method = "namedUnsignedMapResult", + analysisResultsNumberMatcher = eq(count = 1), + { result: GoResult -> result.map == mapOf(ULong.MAX_VALUE to 7L) }, + ) + } + + @Test + @GoSample(method = "mapDeletedDuringRange", fixture = "regressions") + fun mapDeletedDuringRange() = checkNative(method = "mapDeletedDuringRange") + + @Test + @GoSample(method = "mapRangeCurrentValues", fixture = "regressions") + fun mapRangeCurrentValues() = checkNative(method = "mapRangeCurrentValues") + + @Test + @GoSample(method = "mapDeletedReferenceKeysDuringRange", fixture = "regressions") + fun mapDeletedReferenceKeysDuringRange() = checkNative(method = "mapDeletedReferenceKeysDuringRange") + + @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/ConverterTypeIdentityTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ConverterTypeIdentityTest.kt new file mode 100644 index 0000000000..fbb58faeee --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ConverterTypeIdentityTest.kt @@ -0,0 +1,58 @@ +package org.usvm.samples.serialization + +import org.jacodb.go.api.BasicType +import org.jacodb.go.api.NamedType +import org.jacodb.go.api.PointerType +import org.jacodb.go.api.StructType +import org.jacodb.go.api.TupleType +import org.junit.jupiter.api.Test +import org.usvm.model.Converter +import org.usvm.model.Package +import org.usvm.model.Type +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertSame + +class ConverterTypeIdentityTest { + @Test + fun preservesTupleAndStructFieldTypesAcrossApiNameCollisions() { + val types = listOf( + Type.Basic(name = "bool"), + Type.Basic(name = "int"), + Type.Basic(name = "rune"), + Type.Basic(name = "float32"), + Type.Pointer(name = "*int", elem = "int"), + Type.Tuple(name = "floatNext", elems = listOf("bool", "float32", "int")), + Type.Tuple(name = "stringNext", elems = listOf("bool", "int", "rune")), + Type.Struct(name = "scalarRecord", fields = listOf("int")), + Type.Struct(name = "pointerRecord", fields = listOf("*int")), + ) + + val actual = Converter.unpack( + Package(name = "identity", members = emptyList(), types = types.associateBy { it.name }, intSize = 64), + ).types + + val tuple = assertIs(actual.getValue("stringNext")) + assertEquals(listOf("bool", "int", "rune"), tuple.types.map { it.typeName }) + assertIs(assertIs(actual.getValue("scalarRecord")).fields.orEmpty().single()) + assertIs(assertIs(actual.getValue("pointerRecord")).fields.orEmpty().single()) + } + + @Test + fun preservesRecursiveNamedTypeReferences() { + val types = listOf( + Type.Named(name = "node", underlying = "nodeRecord", methods = emptyList()), + Type.Struct(name = "nodeRecord", fields = listOf("*node")), + Type.Pointer(name = "*node", elem = "node"), + ) + + val actual = Converter.unpack( + Package(name = "recursive", members = emptyList(), types = types.associateBy { it.name }, intSize = 64), + ).types + + val named = assertIs(actual.getValue("node")) + val pointer = assertIs(assertIs(named.underlyingType).fields.orEmpty().single()) + assertSame(named, pointer.baseType) + assertSame(pointer, actual.getValue("*node")) + } +} diff --git a/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt similarity index 68% rename from usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt rename to usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt index 8e05bb0ce1..2e2f190e98 100644 --- a/usvm-go/src/test/kotlin/org/usvm/model/ModelTest.kt +++ b/usvm-go/src/test/kotlin/org/usvm/samples/serialization/ModelTest.kt @@ -1,13 +1,15 @@ -package org.usvm.model +package org.usvm.samples.serialization import org.junit.jupiter.api.Test +import org.usvm.generatedGoFile +import org.usvm.model.Parser import java.io.File import kotlin.test.assertEquals class ModelTest { @Test fun testEqualContent() { - val filename = "out/usvm_examples.json" + val filename = generatedGoFile("examples/usvm_examples.json").path val file = File(filename) val parser = Parser() 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..8cd76ed5b6 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/InterfaceRegressionTest.kt @@ -0,0 +1,107 @@ +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.GoSlice +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 = "sliceMethodDoesNotImplementVariadic", fixture = "regressions") + fun sliceMethodDoesNotImplementVariadic() = checkNative(method = "sliceMethodDoesNotImplementVariadic") + + @Test + @GoSample(method = "variadicMethodImplementsVariadic", fixture = "regressions") + fun variadicMethodImplementsVariadic() = checkNative(method = "variadicMethodImplementsVariadic") + + @Test + @GoSample(method = "variadicMethodDoesNotImplementSlice", fixture = "regressions") + fun variadicMethodDoesNotImplementSlice() = checkNative(method = "variadicMethodDoesNotImplementSlice") + + @Test + @GoSample(method = "(usvm/regressions.sliceSignatureReceiver).Count", fixture = "regressions") + fun sliceSignatureReceiverCountsArguments() { + checkDiscoveredProperties( + method = "(usvm/regressions.sliceSignatureReceiver).Count", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: GoStruct, values: GoSlice, result: GoResult -> + result.isSuccess && result.long == (values?.size ?: 0).toLong() + }, + ) + } + + @Test + @GoSample(method = "(usvm/regressions.variadicSignatureReceiver).Count", fixture = "regressions") + fun variadicSignatureReceiverCountsArguments() { + checkDiscoveredProperties( + method = "(usvm/regressions.variadicSignatureReceiver).Count", + analysisResultsNumberMatcher = ignoreNumberOfAnalysisResults, + { _: GoStruct, values: GoSlice, result: GoResult -> + result.isSuccess && result.long == (values?.size ?: 0).toLong() + }, + ) + } + + @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..0782c0b9a4 --- /dev/null +++ b/usvm-go/src/test/kotlin/org/usvm/samples/types/ValueRegressionTest.kt @@ -0,0 +1,89 @@ +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 = "copiedArrayEquality", fixture = "regressions") + fun copiedArrayEquality() = checkNative(method = "copiedArrayEquality") + + @Test + @GoSample(method = "copiedArrayInequality", fixture = "regressions") + fun copiedArrayInequality() = checkNative(method = "copiedArrayInequality") + + @Test + @GoSample(method = "copiedStructEquality", fixture = "regressions") + fun copiedStructEquality() = checkNative(method = "copiedStructEquality") + + @Test + @GoSample(method = "structPointerFieldsCompareIdentity", fixture = "regressions") + fun structPointerFieldsCompareIdentity() = checkNative(method = "structPointerFieldsCompareIdentity") + + @Test + @GoSample(method = "copiedStructSharesPointerIdentity", fixture = "regressions") + fun copiedStructSharesPointerIdentity() = checkNative(method = "copiedStructSharesPointerIdentity") + + @Test + @GoSample(method = "interfaceArrayValueEquality", fixture = "regressions") + fun interfaceArrayValueEquality() = checkNative(method = "interfaceArrayValueEquality") + + @Test + @GoSample(method = "interfaceNamedTypeInequality", fixture = "regressions") + fun interfaceNamedTypeInequality() = checkNative(method = "interfaceNamedTypeInequality") + + @Test + @GoSample(method = "interfaceUncomparablePanics", fixture = "regressions") + fun interfaceUncomparablePanics() = checkNative(method = "interfaceUncomparablePanics") + + @Test + @GoSample(method = "interfaceDifferentUncomparableTypes", fixture = "regressions") + fun interfaceDifferentUncomparableTypes() = checkNative(method = "interfaceDifferentUncomparableTypes") + + @Test + @GoSample(method = "structEqualityStopsBeforeUncomparableField", fixture = "regressions") + fun structEqualityStopsBeforeUncomparableField() = checkNative( + method = "structEqualityStopsBeforeUncomparableField", + ) + + @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 index cf11c9cba0..21170a981d 100644 --- a/usvm-go/src/test/resources/logback.xml +++ b/usvm-go/src/test/resources/logback.xml @@ -6,7 +6,7 @@ - + \ No newline at end of file diff --git a/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt b/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt index 7075521821..2d48201b4f 100644 --- a/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt +++ b/usvm-util/src/main/kotlin/org/usvm/algorithms/WeightedAaTree.kt @@ -132,7 +132,7 @@ class WeightedAaTree(private val comparator: Comparator) { } } - private fun removeRec(value: T, deleteFrom: AaTreeNode?, k: (AaTreeNode?) -> AaTreeNode?): AaTreeNode? { + private tailrec fun removeRec(value: T, deleteFrom: AaTreeNode?, k: (AaTreeNode?) -> AaTreeNode?): AaTreeNode? { if (deleteFrom == null) { return null }