Skip to content

Commit

Permalink
Add chisel-z3-test.sc
Browse files Browse the repository at this point in the history
  • Loading branch information
sinofp committed Aug 13, 2021
1 parent cee8f70 commit 2c87760
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/test.yml
Expand Up @@ -41,3 +41,15 @@ jobs:
- run: ./format.sh
- name: Check changes
run: git diff --quiet
z3-test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: pavpanchekha/setup-z3@0.2.0
with:
version: 4.8.10
- name: Install ammonite & espresso
run: |
sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/com-lihaoyi/Ammonite/releases/download/2.4.0/2.12-2.4.0) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm'
cmake -DBUILD_DOC=NO -B build && sudo make -C build install
- run: ./chisel-z3-test.sc
212 changes: 212 additions & 0 deletions chisel-z3-test.sc
@@ -0,0 +1,212 @@
#!/usr/bin/env amm

interp.repositories() ++= Seq(
coursierapi.MavenRepository.of(
"https://oss.sonatype.org/content/repositories/snapshots"
)
)

@
import $ivy.`edu.berkeley.cs::chisel3:3.5-SNAPSHOT`

import chisel3._
import chisel3.util._
import chisel3.util.experimental.decode._
import chisel3.stage.{ChiselGeneratorAnnotation, ChiselStage}

import firrtl.annotations.Annotation
import firrtl.options.{OutputAnnotationFileAnnotation, TargetDirAnnotation}
import firrtl.stage.OutputFileAnnotation
import firrtl.util.BackendCompilationUtilities.timeStamp

import logger.{LazyLogging, LogLevel, LogLevelAnnotation}
import scala.util.Properties

trait MCResult
case object MCSuccess extends MCResult
case class MCFail(k: Int) extends MCResult

object Z3ModelChecker extends LazyLogging {
def bmc(testDir: os.Path, main: String, kmax: Int): MCResult = {
assert(kmax >= 0 && kmax < 50, "Trying to keep kmax in a reasonable range.")
Seq.tabulate(kmax + 1) { k =>
val stepFile = testDir / s"${main}_step$k.smt2"
os.copy(testDir / s"$main.smt2", stepFile)
os.write.append(
stepFile,
s"""${step(main, k)}
|(check-sat)
|""".stripMargin
)
val success = executeStep(stepFile)
if (!success) return MCFail(k)
}
MCSuccess
}

def executeStep(path: os.Path): Boolean = {
val (out, ret) = executeCmd(path.toString)
assert(
ret == 0,
s"expected success (0), not $ret: `$out`\nz3 ${path.toString}"
)
assert(
out == "sat" + Properties.lineSeparator || out == "unsat" + Properties.lineSeparator,
s"Unexpected output: $out"
)
out == "unsat" + Properties.lineSeparator
}

def executeCmd(cmd: String): (String, Int) = {
val process = os
.proc("z3", cmd)
.call(stderr = os.ProcessOutput.Readlines(logger.warn(_)))
(process.out.chunks.mkString, process.exitCode)
}

def step(main: String, k: Int): String = {
// define all states
(0 to k).map(ii => s"(declare-fun s$ii () $main$StateTpe)") ++
// assert that init holds in state 0
List(s"(assert ($main$Init s0))") ++
// assert transition relation
(0 until k).map(ii => s"(assert ($main$Transition s$ii s${ii + 1}))") ++
// assert that assumptions hold in all states
(0 to k).map(ii => s"(assert ($main$Assumes s$ii))") ++
// assert that assertions hold for all but last state
(0 until k).map(ii => s"(assert ($main$Asserts s$ii))") ++
// check to see if we can violate the assertions in the last state
List(s"(assert (not ($main$Asserts s$k)))")
}.mkString("\n")

// the following suffixes have to match the ones in [[SMTTransitionSystemEncoder]]
val Transition = "_t"
val Init = "_i"
val Asserts = "_a"
val Assumes = "_u"
val StateTpe = "_s"
}

class TestModule(unminimized: TruthTable, minimized: TruthTable)
extends Module {
val i = IO(Input(UInt(unminimized.table.head._1.getWidth.W)))
val (unminimizedI, unminimizedO) = pla(unminimized.table.toSeq)
val (minimizedI, minimizedO) = pla(minimized.table.toSeq)
unminimizedI := i
minimizedI := i

val someKeyMatch =
unminimized.table.map { case (key, _) => i === key }.reduce(_ || _)

chisel3.experimental.verification.assert(
// TODO now ignore default, if input match, output should be the same.
someKeyMatch && (minimizedO === unminimizedO) || !someKeyMatch
// for each instruction, if input matches, output should match, not no matched, fallback to default
// (unminimized.table.map { case (key, value) => (i === key) && (minimizedO === value) } ++
// Seq(unminimized.table.keys.map(i =/= _).reduce(_ && _) && minimizedO === unminimized.default)).reduce(_ || _)
)
}

def test(
dut: () => Module,
name: String,
expected: MCResult,
kmax: Int = 0
): Unit = {
val testBaseDir = os.pwd / "test_run_dir" / name
os.makeDir.all(testBaseDir)
val testDir = os.temp.dir(testBaseDir, timeStamp, deleteOnExit = false)
val res = (new ChiselStage).execute(
Array("-E", "experimental-smt2"),
Seq(
LogLevelAnnotation(LogLevel.Error), // silence warnings for tests
ChiselGeneratorAnnotation(dut),
TargetDirAnnotation(testDir.toString)
)
)
val top = res.collectFirst { case OutputAnnotationFileAnnotation(top) =>
top
}.get
assert(res.collectFirst { case _: OutputFileAnnotation => true }.isDefined)
val r = Z3ModelChecker.bmc(testDir, top, kmax)
assert(r == expected, s"$name failed")
}

def readTable(fileName: os.Path): TruthTable = {
def bitPat(espresso: String): BitPat = BitPat(
"b" + espresso.replace('-', '?')
)

def specialAdd(left: Char, right: Char): Char =
if (left == '0') right
else if (right == '0') left
else if (left == right) left
else 'x' // x is wrong!

val lines = os
.read(fileName)
.split('\n')
.filterNot(_.startsWith("#"))
.map(_.split('#').head)
val inputLength = lines.filter(_.startsWith(".i")).head.split("\\s+")(1).toInt
val outputLength =
lines.filter(_.startsWith(".o")).head.split("\\s+")(1).toInt

val inputOutput = lines
.filterNot(_.startsWith("."))
.mkString
.replaceAll("\\s+|[|]", "")
.replace('2', '-') // 2 is a synonym for -, it can be used in input & output
.grouped(inputLength + outputLength)
.toList
.map(_.splitAt(inputLength))
.map(io =>
(io._1, io._2.replace("~", "0"))
) // in type fd, both ~ & 0 implies "has no meaning". ~ is only used in output

val table = inputOutput
.groupBy(_._1)
.map { case (str, tuples) => str -> tuples.map(_._2) }
.map { case (str, strings) =>
str -> strings.reduce(_.zip(_).map { case (r, l) =>
specialAdd(r, l)
}.mkString)
}
.map { case (str, str1) => bitPat(str) -> bitPat(str1) }

val default = BitPat(
s"b${"0" * table.head._2.getWidth}"
) // TODO: infer from .type

TruthTable(table, default)
}

// format: off
val getX = Seq("spla", "pdc", "alu2", "test3", "test2")
val assertionFail = Seq("misex3c", "bw", "ex1010", "mytest", "inc", "alu3", "dekoder", "wim", "t4", "exp", "dk17", "newxcpla1", "bcd", "t2", "dk27", "mytest3", "bcc", "mark1", "bca", "b10", "bcb", "apla", "dk48", "check2", "mytest2", "exep")
val bad = getX ++ assertionFail ++ Seq("o64")
// format: on

val devNull = os.root / "dev" / "null"
os.walk(os.pwd / "examples")
.filter(os.isFile)
.filterNot(p => bad.contains(p.baseName))
.foreach { inputFile =>
println(s"Testing ${inputFile.baseName} @ $inputFile")
val outputFile = os.temp(prefix = inputFile.baseName)
os.proc("espresso", s"$inputFile")
.call(
stdout = outputFile,
stderr = devNull
) // stderr: span more than one line (warning)
// try {
test(
() => new TestModule(readTable(inputFile), readTable(outputFile)),
inputFile.baseName,
MCSuccess
)
// } catch {
// case e: Exception => print(inputFile); e.printStackTrace()
// case e: Error => print(inputFile); e.printStackTrace()
// }
}

0 comments on commit 2c87760

Please sign in to comment.