aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/firrtl/util/BackendCompilationUtilities.scala
blob: a687cb28a62d4027a7e68994942d1ed37ec743d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
// SPDX-License-Identifier: Apache-2.0

package firrtl.util

import java.io._
import java.nio.file.Files
import java.text.SimpleDateFormat
import java.util.Calendar

import logger.LazyLogging

import firrtl.FileUtils

import scala.sys.process.{ProcessBuilder, ProcessLogger, _}

object BackendCompilationUtilities extends LazyLogging {

  /** Parent directory for tests */
  lazy val TestDirectory = new File("test_run_dir")

  def timeStamp: String = {
    val format = new SimpleDateFormat("yyyyMMddHHmmss")
    val now = Calendar.getInstance.getTime
    format.format(now)
  }

  def loggingProcessLogger: ProcessLogger =
    ProcessLogger(logger.info(_), logger.warn(_))

  /**
    * Copy the contents of a resource to a destination file.
    * @param name the name of the resource
    * @param file the file to write it into
    */
  def copyResourceToFile(name: String, file: File): Unit = {
    val in = getClass.getResourceAsStream(name)
    if (in == null) {
      throw new FileNotFoundException(s"Resource '$name'")
    }
    val out = new FileOutputStream(file)
    Iterator.continually(in.read).takeWhile(-1 != _).foreach(out.write)
    out.close()
  }

  /** Create a test directory
    *
    * Will create outer directory called testName then inner directory based on
    * the current time
    */
  def createTestDirectory(testName: String): File = {
    val outer = new File(TestDirectory, testName)
    outer.mkdirs()
    Files.createTempDirectory(outer.toPath, timeStamp).toFile
  }

  def makeHarness(template: String => String, post: String)(f: File): File = {
    val prefix = f.toString.split("/").last
    val vf = new File(f.toString + post)
    val w = new FileWriter(vf)
    w.write(template(prefix))
    w.close()
    vf
  }

  /**
    * compule chirrtl to verilog by using a separate process
    *
    * @param prefix basename of the file
    * @param dir    directory where file lives
    * @return       true if compiler completed successfully
    */
  def firrtlToVerilog(prefix: String, dir: File): ProcessBuilder = {
    Process(Seq("firrtl", "-i", s"$prefix.fir", "-o", s"$prefix.v", "-X", "verilog"), dir)
  }

  /** Generates a Verilator invocation to convert Verilog sources to C++
    * simulation sources.
    *
    * The Verilator prefix will be V\$dutFile, and running this will generate
    * C++ sources and headers as well as a makefile to compile them.
    *
    * Verilator will automatically locate the top-level module as the one among
    * all the files which are not included elsewhere. If multiple ones exist,
    * the compilation will fail.
    *
    * If the file BlackBoxSourceHelper.fileListName (or an overridden .f resource filename that is
    * specified with the optional resourceFileName parameter) exists in the output directory,
    * it contains a list of source files to be included. Filter out any files in the vSources
    * sequence that are in this file so we don't include the same file multiple times.
    * This complication is an attempt to work-around the fact that clients used to have to
    * explicitly include additional Verilog sources. Now, more of that is automatic.
    *
    * @param dutFile name of the DUT .v without the .v extension
    * @param dir output directory
    * @param vSources list of additional Verilog sources to compile
    * @param cppHarness C++ testharness to compile/link against
    * @param suppressVcd specifies if VCD tracing should be suppressed
    * @param resourceFileName specifies what filename to look for to find a .f file
    * @param extraCmdLineArgs list of additional command line arguments
    */
  def verilogToCpp(
    dutFile:          String,
    dir:              File,
    vSources:         Seq[File],
    cppHarness:       File,
    suppressVcd:      Boolean = false,
    resourceFileName: String = firrtl.transforms.BlackBoxSourceHelper.defaultFileListName,
    extraCmdLineArgs: Seq[String] = Seq.empty
  ): ProcessBuilder = {

    val topModule = dutFile

    val list_file = new File(dir, resourceFileName)
    val blackBoxVerilogList = {
      if (list_file.exists()) {
        Seq("-f", list_file.getAbsolutePath)
      } else {
        Seq.empty[String]
      }
    }

    // Don't include the same file multiple times.
    // If it's in the main .f resource file, don't explicitly include it on the command line.
    // Build a set of canonical file paths to use as a filter to exclude already included additional Verilog sources.
    val blackBoxHelperFiles: Set[String] = {
      if (list_file.exists()) {
        FileUtils.getLines(list_file).toSet
      } else {
        Set.empty
      }
    }
    val vSourcesFiltered = vSources.filterNot(f => blackBoxHelperFiles.contains(f.getCanonicalPath))
    val command = Seq(
      "verilator",
      "--cc",
      s"${dir.getAbsolutePath}/$dutFile.v"
    ) ++
      extraCmdLineArgs ++
      blackBoxVerilogList ++
      vSourcesFiltered.flatMap(file => Seq("-v", file.getCanonicalPath)) ++
      Seq("--assert", "-Wno-fatal", "-Wno-WIDTH", "-Wno-STMTDLY") ++ {
      if (suppressVcd) { Seq.empty }
      else { Seq("--trace") }
    } ++
      Seq(
        "-O1",
        "--top-module",
        topModule,
        "+define+TOP_TYPE=V" + dutFile,
        s"+define+PRINTF_COND=!$topModule.reset",
        s"+define+STOP_COND=!$topModule.reset",
        "-CFLAGS",
        s"""-Wno-undefined-bool-conversion -O1 -DTOP_TYPE=V$dutFile -DVL_USER_FINISH -include V$dutFile.h""",
        "-Mdir",
        dir.getAbsolutePath,
        "--exe",
        cppHarness.getAbsolutePath
      )
    logger.info(s"${command.mkString(" ")}")
    command
  }

  def cppToExe(prefix: String, dir: File): ProcessBuilder =
    Seq("make", "-C", dir.toString, "-j", "-f", s"V$prefix.mk", s"V$prefix")

  def executeExpectingFailure(
    prefix:       String,
    dir:          File,
    assertionMsg: String = ""
  ): Boolean = {
    var triggered = false
    val assertionMessageSupplied = assertionMsg != ""
    val e = Process(s"./V$prefix", dir) !
      ProcessLogger(
        line => {
          triggered = triggered || (assertionMessageSupplied && line.contains(assertionMsg))
          logger.info(line)
        },
        logger.warn(_)
      )
    // Fail if a line contained an assertion or if we get a non-zero exit code
    //  or, we get a SIGABRT (assertion failure) and we didn't provide a specific assertion message
    triggered || (e != 0 && (e != 134 || !assertionMessageSupplied))
  }

  def executeExpectingSuccess(prefix: String, dir: File): Boolean = {
    !executeExpectingFailure(prefix, dir)
  }

  /** Creates and runs a Yosys script that creates and runs SAT on a miter
    * circuit. Returns true if SAT succeeds, false otherwise
    *
    * The custom and reference Verilog files must not contain any modules with
    * the same name otherwise Yosys will not be able to create a miter circuit
    *
    * @param customTop    name of the DUT with custom transforms without the .v
    *                     extension
    * @param referenceTop name of the DUT without custom transforms without the
    *                     .v extension
    * @param testDir      directory containing verilog files
    * @param timesteps    the maximum number of timesteps for Yosys equivalence
    *                     checking to consider
    */
  def yosysExpectSuccess(customTop: String, referenceTop: String, testDir: File, timesteps: Int = 1): Boolean = {
    !yosysExpectFailure(customTop, referenceTop, testDir, timesteps)
  }

  /** Creates and runs a Yosys script that creates and runs SAT on a miter
    * circuit. Returns false if SAT succeeds, true otherwise
    *
    * The custom and reference Verilog files must not contain any modules with
    * the same name otherwise Yosys will not be able to create a miter circuit
    *
    * @param customTop    name of the DUT with custom transforms without the .v
    *                     extension
    * @param referenceTop name of the DUT without custom transforms without the
    *                     .v extension
    * @param testDir      directory containing verilog files
    * @param timesteps    the maximum number of timesteps for Yosys equivalence
    *                     checking to consider
    */
  def yosysExpectFailure(customTop: String, referenceTop: String, testDir: File, timesteps: Int = 1): Boolean = {

    val scriptFileName = s"${testDir.getAbsolutePath}/yosys_script"
    val yosysScriptWriter = new PrintWriter(scriptFileName)
    yosysScriptWriter.write(s"""read_verilog ${testDir.getAbsolutePath}/$customTop.v
                               |prep -flatten -top $customTop; proc; opt; memory
                               |design -stash custom
                               |read_verilog ${testDir.getAbsolutePath}/$referenceTop.v
                               |prep -flatten -top $referenceTop; proc; opt; memory
                               |design -stash reference
                               |design -copy-from custom -as custom $customTop
                               |design -copy-from reference -as reference $referenceTop
                               |equiv_make custom reference equiv
                               |hierarchy -top equiv
                               |prep -flatten -top equiv
                               |clean -purge
                               |equiv_simple -seq $timesteps
                               |equiv_induct -seq $timesteps
                               |equiv_status -assert
         """.stripMargin)
    yosysScriptWriter.close()

    val resultFileName = testDir.getAbsolutePath + "/yosys_results"
    val command = s"yosys -s $scriptFileName" #> new File(resultFileName)
    command.! != 0
  }
}