Skip to content

Commit

Permalink
Smt-lib benchmarks based tests (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Saloed committed Sep 16, 2022
1 parent 0b56fac commit 86417db
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 10 deletions.
1 change: 1 addition & 0 deletions detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ complexity:
ignoreArgumentsMatchingNames: false
NestedBlockDepth:
active: true
excludes: [ '**/test/**', '**/example/**' ]
threshold: 4
ReplaceSafeCallChainWithRun:
active: false
Expand Down
20 changes: 15 additions & 5 deletions ksmt-core/src/main/kotlin/org/ksmt/KContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -504,14 +504,24 @@ open class KContext {
fun mkBV(value: Int): KBitVec32Expr = bv32Cache.create(value)
fun mkBV(value: Long): KBitVec64Expr = bv64Cache.create(value)
fun mkBV(value: String, sizeBits: UInt): KBitVecExpr<KBVSort> = when (sizeBits.toInt()) {
1 -> mkBV(value.toInt(radix = 2) != 0).cast()
Byte.SIZE_BITS -> mkBV(value.toByte(radix = 2)).cast()
Short.SIZE_BITS -> mkBV(value.toShort(radix = 2)).cast()
Int.SIZE_BITS -> mkBV(value.toInt(radix = 2)).cast()
Long.SIZE_BITS -> mkBV(value.toLong(radix = 2)).cast()
1 -> mkBV(value.parseBvNumber(sizeBits) != 0L).cast()
Byte.SIZE_BITS -> mkBV(value.parseBvNumber(sizeBits).toByte()).cast()
Short.SIZE_BITS -> mkBV(value.parseBvNumber(sizeBits).toShort()).cast()
Int.SIZE_BITS -> mkBV(value.parseBvNumber(sizeBits).toInt()).cast()
Long.SIZE_BITS -> mkBV(value.parseBvNumber(sizeBits)).cast()
else -> bvCache.create(value, sizeBits)
}

/** In Kotlin "1".repeat(64).toLong(radix = 2) is [NumberFormatException], but we expect it to be -1.
* To overcome this problem, we use [java.lang.Long.parseUnsignedLong]
* */
private fun String.parseBvNumber(sizeBits: UInt): Long {
require(length.toUInt() <= sizeBits) {
"Provided string $this consist of $length symbols, which is greater than $sizeBits"
}
return java.lang.Long.parseUnsignedLong(this, 2)
}

// quantifiers
private val existentialQuantifierCache = mkCache { body: KExpr<KBoolSort>, bounds: List<KDecl<*>> ->
ensureContextMatch(body)
Expand Down
71 changes: 66 additions & 5 deletions ksmt-z3/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import org.gradle.language.jvm.tasks.ProcessResources
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.6.21"
id("io.gitlab.arturbosch.detekt") version "1.20.0"
id("de.undercouch.download") version "5.1.0"
}

repositories {
Expand All @@ -17,6 +17,7 @@ dependencies {
implementation(project(":ksmt-core"))
implementation("org.sosy-lab", "javasmt-solver-z3", "4.8.9-sosy1")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.8.2")
z3native("com.microsoft.z3", "z3-native-win64", "4.8.9.1", ext = "zip")
z3native("com.microsoft.z3", "z3-native-linux64", "4.8.9.1", ext = "zip")
z3native("com.microsoft.z3", "z3-native-osx", "4.8.9.1", ext = "zip")
Expand All @@ -31,14 +32,74 @@ detekt {
config = files(rootDir.resolve("detekt.yml"))
}

tasks.withType<Test> {
useJUnitPlatform()
}

tasks.withType<ProcessResources> {
z3native.resolvedConfiguration.resolvedArtifacts.forEach { artifact ->
from(zipTree(artifact.file)) {
into("lib/x64")
}
}
}

// skip big benchmarks to achieve faster tests build and run time
val skipBigBenchmarks = true

val smtLibBenchmarks = listOfNotNull(
"QF_ALIA", // 12M
"QF_AUFLIA", // 1.5M
if (!skipBigBenchmarks) "QF_LIA" else null, // 5.2G
"QF_LIRA", // 500K
if (!skipBigBenchmarks) "QF_LRA" else null, // 1.1G
if (!skipBigBenchmarks) "QF_UF" else null, // 100M
"QF_UFLIA",// 1.3M
if (!skipBigBenchmarks) "QF_UFLRA" else null, // 422M
"ALIA", // 400K
"AUFLIA", // 12M
"AUFLIRA", // 19M
"LIA", // 1.2M
"LRA", // 4.5M
// "UFLIA", // 64M // skipped, because systematically fails to download
"UFLRA", // 276K
"ABV", // 400K
if (!skipBigBenchmarks) "AUFBV" else null, // 1.2G
if (!skipBigBenchmarks) "BV" else null, // 847M
if (!skipBigBenchmarks) "QF_ABV" else null, // 253M
if (!skipBigBenchmarks) "QF_BV" else null// 12.3G
)

val smtLibBenchmarkTestData = smtLibBenchmarks.map { mkSmtLibBenchmarkTestData(it) }

val prepareTestData by tasks.registering {
dependsOn.addAll(smtLibBenchmarkTestData)
}

tasks.withType<Test> {
useJUnitPlatform()
dependsOn.add(prepareTestData)
}

fun mkSmtLibBenchmarkTestData(name: String) = tasks.register("smtLibBenchmark-$name") {
doLast {
val path = buildDir.resolve("smtLibBenchmark/$name")
val downloadTarget = path.resolve("$name.zip")
val repoUrl = "https://clc-gitlab.cs.uiowa.edu:2443"
val url = "$repoUrl/api/v4/projects/SMT-LIB-benchmarks%2F$name/repository/archive.zip"
download.run {
src(url)
dest(downloadTarget)
overwrite(false)
}
copy {
from(zipTree(downloadTarget))
into(path)
}
val smtFiles = path.walkTopDown().filter { it.extension == "smt2" }.toList()
val testResources = sourceSets.test.get().output.resourcesDir!!
val testData = testResources.resolve("testData")
copy {
from(smtFiles.toTypedArray())
into(testData)
rename { "${name}_$it" }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
}
}
185 changes: 185 additions & 0 deletions ksmt-z3/src/test/kotlin/org/ksmt/solver/z3/BenchmarksBasedTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package org.ksmt.solver.z3

import com.microsoft.z3.BoolExpr
import com.microsoft.z3.Context
import com.microsoft.z3.Expr
import com.microsoft.z3.Solver
import com.microsoft.z3.Status
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.ksmt.KContext
import org.ksmt.expr.KExpr
import org.ksmt.solver.KSolverStatus
import org.ksmt.sort.KBoolSort
import org.ksmt.sort.KSort
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.relativeTo
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit

class BenchmarksBasedTest {

@ParameterizedTest(name = "{0}")
@MethodSource("testData")
fun testConverter(name: String, samplePath: Path) = skipNotImplementedFeatures {
val ctx = KContext()
Context().use { parseCtx ->
val assertions = parseCtx.parseFile(samplePath)
val ksmtAssertions = run {
val internCtx = KZ3InternalizationContext()
val converter = KZ3ExprConverter(ctx, internCtx)
with(converter) { assertions.map { it.convert<KBoolSort>() } }
}

Context().use { checkCtx ->
checkCtx.performEqualityChecks(ctx) {
for ((originalZ3Expr, ksmtExpr) in assertions.zip(ksmtAssertions)) {
val internalizedExpr = internalize(ksmtExpr)
val z3Expr = originalZ3Expr.translate(checkCtx)
assertTrue("expressions are not equal") {
areEqual(internalizedExpr, z3Expr)
}
}
}
}
}
}

@Disabled // disabled due to a very long runtime
@ParameterizedTest(name = "{0}")
@MethodSource("testData")
fun testSolver(name: String, samplePath: Path) = skipNotImplementedFeatures {
val ctx = KContext()
Context().use { parseCtx ->
val assertions = parseCtx.parseFile(samplePath)
val (expectedStatus, expectedModel) = with(parseCtx) {
val solver = mkSolver().apply {
val params = mkParams().apply {
add("timeout", 1.seconds.toInt(DurationUnit.MILLISECONDS))
}
setParameters(params)
}
solver.add(*assertions)
val status = solver.check()
when (status) {
Status.SATISFIABLE -> KSolverStatus.SAT to solver.model
Status.UNSATISFIABLE -> KSolverStatus.UNSAT to null
Status.UNKNOWN, null -> return
}
}
val ksmtAssertions = run {
val internCtx = KZ3InternalizationContext()
val converter = KZ3ExprConverter(ctx, internCtx)
with(converter) { assertions.map { it.convert<KBoolSort>() } }
}

KZ3Solver(ctx).use { solver ->
ksmtAssertions.forEach { solver.assert(it) }
// use greater timeout to avoid false-positive unknowns
val status = solver.check(timeout = 2.seconds)
assertEquals(expectedStatus, status, "solver check-sat mismatch")

if (status != KSolverStatus.SAT || expectedModel == null) return

val model = solver.model()
val expectedModelAssignments = run {
val internCtx = KZ3InternalizationContext()
val converter = KZ3ExprConverter(ctx, internCtx)
val z3Constants = expectedModel.decls.map { parseCtx.mkConst(it) }
val assignments = z3Constants.associateWith { expectedModel.eval(it, false) }
with(converter) { assignments.map { (const, value) -> const.convert<KSort>() to value } }
}
val assignmentsToCheck = expectedModelAssignments.map { (const, expectedValue) ->
val actualValue = model.eval(const, complete = false)
Triple(const, expectedValue, actualValue)
}

Context().use { checkCtx ->
checkCtx.performEqualityChecks(ctx) {
for ((const, expectedValue, actualValue) in assignmentsToCheck) {
val internalizedExpr = internalize(actualValue)
val z3Expr = expectedValue.translate(checkCtx)
assertTrue("model assignments for $const are not equal") {
areEqual(internalizedExpr, z3Expr)
}
}
}
}
}
}
}

private inline fun skipNotImplementedFeatures(body: () -> Unit) = try {
body()
} catch (ex: NotImplementedError) {
val reducedStackTrace = ex.stackTrace.take(5).joinToString("\n") { it.toString() }
val report = "${ex.message}\n$reducedStackTrace"
System.err.println(report)
// skip test with not implemented feature
Assumptions.assumeTrue(false, ex.message)
}

private fun Context.performEqualityChecks(ctx: KContext, checks: EqualityChecker.() -> Unit) {
val solver = mkSolver()
val params = mkParams().apply {
add("timeout", 1.seconds.toInt(DurationUnit.MILLISECONDS))
}
solver.setParameters(params)
val checker = EqualityChecker(this, solver, ctx)
checker.checks()
}

private class EqualityChecker(
private val ctx: Context,
private val solver: Solver,
ksmtCtx: KContext
) {
private val internCtx = KZ3InternalizationContext()
private val sortInternalizer = KZ3SortInternalizer(ctx, internCtx)
private val declInternalizer = KZ3DeclInternalizer(ctx, internCtx, sortInternalizer)
private val internalizer = KZ3ExprInternalizer(ksmtCtx, ctx, internCtx, sortInternalizer, declInternalizer)

fun internalize(expr: KExpr<*>): Expr = with(internalizer) { expr.internalize() }

fun areEqual(left: Expr, right: Expr): Boolean {
solver.push()
solver.add(ctx.mkNot(ctx.mkEq(left, right)))
val status = solver.check()
solver.pop()
return status == Status.UNSATISFIABLE
}
}

private fun Context.parseFile(path: Path): Array<BoolExpr> = parseSMTLIB2File(
path.toAbsolutePath().toString(),
emptyArray(),
emptyArray(),
emptyArray(),
emptyArray()
)

companion object {

init {
// ensure loading of native library provided by ksmt
KZ3Solver(KContext()).close()
}

@JvmStatic
fun testData(): List<Arguments> {
val testData = this::class.java.classLoader
.getResource("testData")?.toURI()
?.let { Path(it.path) }
?: error("No test data")
val testDataFiles = testData.listDirectoryEntries("*.smt2").sorted()
return testDataFiles.map { Arguments.of(it.relativeTo(testData).toString(), it) }
}
}
}

0 comments on commit 86417db

Please sign in to comment.